Source: jsxc.lib.gui.js

/* global Favico, emojione*/
/**
 * Handle functions for chat window's and buddylist
 *
 * @namespace jsxc.gui
 */
jsxc.gui = {
   /** Smilie token to file mapping */
   emotions: [
      ['O:-) O:)', 'innocent'],
      ['>:-( >:( >:-( >:(', 'angry'],
      [':-) :)', 'slight_smile'],
      [':-D :D', 'grin'],
      [':-( :(', 'disappointed'],
      [';-) ;)', 'wink'],
      [':-P :P', 'stuck_out_tongue'],
      ['=-O', 'astonished'],
      [':kiss: :-*', 'kissing_heart'],
      ['8-) :cool:', 'sunglasses'],
      [':-X :X', 'zipper_mouth'],
      [':yes:', 'thumbsup'],
      [':no:', 'thumbsdown'],
      [':beer:', 'beer'],
      [':coffee:', 'coffee'],
      [':devil:', 'smiling_imp'],
      [':kiss: :kissing:', 'kissing'],
      ['@->-- @->--', 'rose'],
      [':music:', 'musical_note'],
      [':love:', 'heart_eyes'],
      [':heart:', 'heart'],
      [':brokenheart:', 'broken_heart'],
      [':zzz:', 'zzz'],
      [':wait:', 'hand_splayed']
   ],

   favicon: null,

   regShortNames: null,

   emoticonList: {
      'core': {
         ':klaus:': ['klaus'],
         ':jabber:': ['jabber'],
         ':xmpp:': ['xmpp'],
         ':jsxc:': ['jsxc'],
         ':owncloud:': ['owncloud'],
         ':nextcloud:': ['nextcloud']
      },
      'emojione': emojione.emojioneList
   },

   /**
    * Different uri query actions as defined in XEP-0147.
    *
    * @namespace jsxc.gui.queryActions
    */
   queryActions: {
      /** xmpp:JID?message[;body=TEXT] */
      message: function(jid, params) {
         var bid = jsxc.jidToBid(jid);

         if (!jsxc.storage.getUserItem('buddy', bid)) {
            // init contact
            jsxc.storage.saveBuddy(bid, {
               jid: jid,
               name: bid,
               status: 0,
               sub: 'none',
               res: [],
               rnd: Math.random()
            });
         }

         var win = jsxc.gui.window.open(bid);

         if (params && typeof params.body === 'string') {
            win.find('.jsxc_textinput').val(params.body);
         }
      },

      /** xmpp:JID?remove */
      remove: function(jid) {
         jsxc.gui.showRemoveDialog(jsxc.jidToBid(jid));
      },

      /** xmpp:JID?subscribe[;name=NAME] */
      subscribe: function(jid, params) {
         jsxc.gui.showContactDialog(jid);

         if (params && typeof params.name) {
            $('#jsxc_alias').val(params.name);
         }
      },

      /** xmpp:JID?vcard */
      vcard: function(jid) {
         jsxc.gui.showVcard(jid);
      },

      /** xmpp:JID?join[;password=TEXT] */
      join: function(jid, params) {
         var password = (params && params.password) ? params.password : null;

         jsxc.muc.showJoinChat(jid, password);
      }
   },

   /**
    * Creates application skeleton.
    *
    * @memberOf jsxc.gui
    */
   init: function() {
      // Prevent duplicate windowList
      if ($('#jsxc_windowList').length > 0) {
         return;
      }

      jsxc.changeUIState(jsxc.CONST.UISTATE.INITIATING);

      jsxc.gui.regShortNames = new RegExp(emojione.regShortNames.source + '|(' + Object.keys(jsxc.gui.emoticonList.core).join('|') + ')', 'gi');

      $('body').append($(jsxc.gui.template.get('windowList')));

      $(window).resize(jsxc.gui.updateWindowListSB);
      $('#jsxc_windowList').resize(jsxc.gui.updateWindowListSB);

      $('#jsxc_windowListSB .jsxc_scrollLeft').click(function() {
         jsxc.gui.scrollWindowListBy(-200);
      });
      $('#jsxc_windowListSB .jsxc_scrollRight').click(function() {
         jsxc.gui.scrollWindowListBy(200);
      });
      $('#jsxc_windowList').on('wheel', function(ev) {
         if ($('#jsxc_windowList').data('isOver')) {
            jsxc.gui.scrollWindowListBy((ev.originalEvent.wheelDelta > 0) ? 200 : -200);
         }
      });

      jsxc.gui.tooltip('#jsxc_windowList');

      var fo = jsxc.options.get('favicon');
      if (fo && fo.enable) {
         jsxc.gui.favicon = new Favico({
            animation: 'pop',
            bgColor: fo.bgColor,
            textColor: fo.textColor
         });

         jsxc.gui.favicon.badge(jsxc.storage.getUserItem('unreadMsg') || 0);
      }

      if (!jsxc.el_exists('#jsxc_roster')) {
         jsxc.gui.roster.init();
      }

      // prepare regexp for emotions
      $.each(jsxc.gui.emotions, function(i, val) {
         // escape characters
         var reg = val[0].replace(/(\/|\||\*|\.|\+|\?|\^|\$|\(|\)|\[|\]|\{|\})/g, '\\$1');
         reg = '(' + reg.split(' ').join('|') + ')';
         jsxc.gui.emotions[i][2] = new RegExp(reg, 'g');
      });

      // We need this often, so we creates some template jquery objects
      jsxc.gui.windowTemplate = $(jsxc.gui.template.get('chatWindow'));
      jsxc.gui.buddyTemplate = $(jsxc.gui.template.get('rosterBuddy'));
   },

   /**
    * Init tooltip plugin for given jQuery selector.
    *
    * @param {String} selector jQuery selector
    * @memberOf jsxc.gui
    */
   tooltip: function(selector) {
      $(selector).tooltip({
         show: {
            delay: 600
         },
         content: function() {
            return $(this).attr('title').replace(/\n/g, '<br />');
         }
      });
   },

   /**
    * Updates Information in roster and chatbar
    *
    * @param {String} bid bar jid
    */
   update: function(bid) {
      var data = jsxc.storage.getUserItem('buddy', bid);

      if (!data) {
         jsxc.debug('No data for ' + bid);
         return;
      }

      var ri = jsxc.gui.roster.getItem(bid); // roster item from user
      var we = jsxc.gui.window.get(bid); // window element from user
      var ue = ri.add(we); // both
      var spot = $('.jsxc_spot[data-bid="' + bid + '"]');

      // Attach data to corresponding roster item
      ri.data(data);

      // Add online status
      jsxc.gui.updatePresence(bid, jsxc.CONST.STATUS[data.status]);

      // Change name and add title
      ue.find('.jsxc_name:first').add(spot).text(data.name).attr('title', $.t('is_', {
         status: $.t(jsxc.CONST.STATUS[data.status])
      }));

      // Update gui according to encryption state
      switch (data.msgstate) {
         case 0:
            we.find('.jsxc_transfer').removeClass('jsxc_enc jsxc_fin').attr('title', $.t('your_connection_is_unencrypted'));
            we.find('.jsxc_settings .jsxc_verification').addClass('jsxc_disabled');
            we.find('.jsxc_settings .jsxc_transfer').text($.t('start_private'));
            break;
         case 1:
            we.find('.jsxc_transfer').addClass('jsxc_enc').attr('title', $.t('your_connection_is_encrypted'));
            we.find('.jsxc_settings .jsxc_verification').removeClass('jsxc_disabled');
            we.find('.jsxc_settings .jsxc_transfer').text($.t('close_private'));
            break;
         case 2:
            we.find('.jsxc_settings .jsxc_verification').addClass('jsxc_disabled');
            we.find('.jsxc_transfer').removeClass('jsxc_enc').addClass('jsxc_fin').attr('title', $.t('your_buddy_closed_the_private_connection'));
            we.find('.jsxc_settings .jsxc_transfer').text($.t('close_private'));
            break;
      }

      // update gui according to verification state
      if (data.trust) {
         we.find('.jsxc_transfer').addClass('jsxc_trust').attr('title', $.t('your_buddy_is_verificated'));
      } else {
         we.find('.jsxc_transfer').removeClass('jsxc_trust');
      }

      // update gui according to subscription state
      if (data.sub && data.sub !== 'both') {
         ue.addClass('jsxc_oneway');
      } else {
         ue.removeClass('jsxc_oneway');
      }

      var info = Strophe.getBareJidFromJid(data.jid) + '\n';
      info += $.t('Subscription') + ': ' + $.t(data.sub) + '\n';
      info += $.t('Status') + ': ' + $.t(jsxc.CONST.STATUS[data.status]);

      ri.find('.jsxc_name').attr('title', info);

      jsxc.gui.avatar.update(ri.add(we.find('.jsxc_bar')), data.jid, data.avatar);

      $(document).trigger('update.gui.jsxc', [bid]);
   },

   /**
    * Updates scrollbar handlers.
    *
    * @memberOf jsxc.gui
    */
   updateWindowListSB: function() {

      if ($('#jsxc_windowList>ul').width() > $('#jsxc_windowList').width()) {
         $('#jsxc_windowListSB > div').removeClass('jsxc_disabled');
      } else {
         $('#jsxc_windowListSB > div').addClass('jsxc_disabled');
         $('#jsxc_windowList>ul').css('right', '0px');
      }
   },

   /**
    * Scroll window list by offset.
    *
    * @memberOf jsxc.gui
    * @param offset
    */
   scrollWindowListBy: function(offset) {

      var scrollWidth = $('#jsxc_windowList>ul').width();
      var width = $('#jsxc_windowList').width();
      var el = $('#jsxc_windowList>ul');
      var right = parseInt(el.css('right')) - offset;
      var padding = $("#jsxc_windowListSB").width();

      if (scrollWidth < width) {
         return;
      }

      if (right > 0) {
         right = 0;
      }

      if (right < width - scrollWidth - padding) {
         right = width - scrollWidth - padding;
      }

      el.css('right', right + 'px');
   },

   /**
    * Returns the window element
    *
    * @deprecated Use {@link jsxc.gui.window.get} instead.
    * @param {String} bid
    * @returns {jquery} jQuery object of the window element
    */
   getWindow: function(bid) {
      jsxc.warn('jsxc.gui.getWindow is deprecated!');

      return jsxc.gui.window.get(bid);
   },

   /**
    * Toggle list with timeout, like menu or settings
    *
    * @memberof jsxc.gui
    */
   toggleList: function(el) {
      var self = el || $(this);

      self.disableSelection();

      self.addClass('jsxc_list');

      var ul = self.find('ul');
      var slideUp = null;

      slideUp = function() {

         self.removeClass('jsxc_opened');

         $('body').off('click', null, slideUp);
      };

      $(this).click(function() {

         if (!self.hasClass('jsxc_opened')) {
            // hide other lists
            $('body').click();
            $('body').one('click', slideUp);
         } else {
            $('body').off('click', null, slideUp);
         }

         window.clearTimeout(ul.data('timer'));

         self.toggleClass('jsxc_opened');

         return false;
      }).mouseleave(function() {
         ul.data('timer', window.setTimeout(slideUp, 2000));
      }).mouseenter(function() {
         window.clearTimeout(ul.data('timer'));
      });
   },

   /**
    * Creates and show loginbox
    */
   showLoginBox: function() {
      // Set focus to username or password field
      $(document).one("complete.dialog.jsxc", function() {
         setTimeout(function() {
            if ($("#jsxc_username").val().length === 0) {
               $("#jsxc_username").focus();
            } else {
               $('#jsxc_password').focus();
            }
         }, 50);
      });

      jsxc.gui.dialog.open(jsxc.gui.template.get('loginBox'));

      var alert = $('#jsxc_dialog').find('.jsxc_alert');
      alert.hide();

      $('#jsxc_dialog').find('form').submit(function(ev) {

         ev.preventDefault();

         $(this).find('button[data-jsxc-loading-text]').trigger('btnloading.jsxc');

         jsxc.options.loginForm.form = $(this);
         jsxc.options.loginForm.jid = $(this).find('#jsxc_username');
         jsxc.options.loginForm.pass = $(this).find('#jsxc_password');

         jsxc.triggeredFromBox = true;
         jsxc.options.loginForm.triggered = false;

         jsxc.prepareLogin(function(settings) {
            if (settings === false) {
               onAuthFail();
            } else {
               $(document).on('authfail.jsxc', onAuthFail);

               jsxc.xmpp.login();
            }
         });
      });

      function onAuthFail() {
         alert.show();
         jsxc.gui.dialog.resize();

         $('#jsxc_dialog').find('button').trigger('btnfinished.jsxc');

         $('#jsxc_dialog').find('input').one('keypress', function() {
            alert.hide();
            jsxc.gui.dialog.resize();
         });
      }
   },

   /**
    * Creates and show the fingerprint dialog
    *
    * @param {String} bid
    */
   showFingerprints: function(bid) {
      jsxc.gui.dialog.open(jsxc.gui.template.get('fingerprintsDialog', bid));
   },

   /**
    * Creates and show the verification dialog
    *
    * @param {String} bid
    */
   showVerification: function(bid) {

      // Check if there is a open dialog
      if ($('#jsxc_dialog').length > 0) {
         setTimeout(function() {
            jsxc.gui.showVerification(bid);
         }, 3000);
         return;
      }

      // verification only possible if the connection is encrypted
      if (jsxc.storage.getUserItem('buddy', bid).msgstate !== OTR.CONST.MSGSTATE_ENCRYPTED) {
         jsxc.warn('Connection not encrypted');
         return;
      }

      jsxc.gui.dialog.open(jsxc.gui.template.get('authenticationDialog', bid), {
         name: 'smp'
      });

      // Add handler

      $('#jsxc_dialog > div:gt(0)').hide();
      $('#jsxc_dialog > div:eq(0) button').click(function() {

         $(this).siblings().removeClass('active');
         $(this).addClass('active');
         $(this).get(0).blur();

         $('#jsxc_dialog > div:gt(0)').hide();
         $('#jsxc_dialog > div:eq(' + ($(this).index() + 1) + ')').show().find('input:first').focus();
      });

      // Manual
      $('#jsxc_dialog > div:eq(1) .jsxc_submit').click(function() {
         if (jsxc.master) {
            jsxc.otr.objects[bid].trust = true;
         }

         jsxc.storage.updateUserItem('buddy', bid, 'trust', true);

         jsxc.gui.dialog.close('smp');

         jsxc.storage.updateUserItem('buddy', bid, 'trust', true);
         jsxc.gui.window.postMessage({
            bid: bid,
            direction: jsxc.Message.SYS,
            msg: $.t('conversation_is_now_verified')
         });
         jsxc.gui.update(bid);
      });

      // Question
      $('#jsxc_dialog > div:eq(2) .jsxc_submit').click(function() {
         var div = $('#jsxc_dialog > div:eq(2)');
         var sec = div.find('#jsxc_secret2').val();
         var quest = div.find('#jsxc_quest').val();

         if (sec === '' || quest === '') {
            // Add information for the user which form is missing
            div.find('input[value=""]').addClass('jsxc_invalid').keyup(function() {
               if ($(this).val().match(/.*/)) {
                  $(this).removeClass('jsxc_invalid');
               }
            });
            return;
         }

         if (jsxc.master) {
            jsxc.otr.sendSmpReq(bid, sec, quest);
         } else {
            jsxc.storage.setUserItem('smp', bid, {
               sec: sec,
               quest: quest
            });
         }

         jsxc.gui.dialog.close('smp');

         jsxc.gui.window.postMessage({
            bid: bid,
            direction: jsxc.Message.SYS,
            msg: $.t('authentication_query_sent')
         });
      });

      // Secret
      $('#jsxc_dialog > div:eq(3) .jsxc_submit').click(function() {
         var div = $('#jsxc_dialog > div:eq(3)');
         var sec = div.find('#jsxc_secret').val();

         if (sec === '') {
            // Add information for the user which form is missing
            div.find('#jsxc_secret').addClass('jsxc_invalid').keyup(function() {
               if ($(this).val().match(/.*/)) {
                  $(this).removeClass('jsxc_invalid');
               }
            });
            return;
         }

         if (jsxc.master) {
            jsxc.otr.sendSmpReq(bid, sec);
         } else {
            jsxc.storage.setUserItem('smp', bid, {
               sec: sec,
               quest: null
            });
         }

         jsxc.gui.dialog.close('smp');

         jsxc.gui.window.postMessage({
            bid: bid,
            direction: 'sys',
            msg: $.t('authentication_query_sent')
         });
      });
   },

   /**
    * Create and show approve dialog
    *
    * @param {type} from valid jid
    */
   showApproveDialog: function(from) {
      jsxc.gui.dialog.open(jsxc.gui.template.get('approveDialog'), {
         'noClose': true
      });

      $('#jsxc_dialog .jsxc_their_jid').text(Strophe.getBareJidFromJid(from));

      $('#jsxc_dialog .jsxc_deny').click(function(ev) {
         ev.stopPropagation();

         jsxc.xmpp.resFriendReq(from, false);

         jsxc.gui.dialog.close();
      });

      $('#jsxc_dialog .jsxc_approve').click(function(ev) {
         ev.stopPropagation();

         var data = jsxc.storage.getUserItem('buddy', jsxc.jidToBid(from));

         jsxc.xmpp.resFriendReq(from, true);

         // If friendship is not mutual show contact dialog
         if (!data || data.sub === 'from') {
            jsxc.gui.showContactDialog(from);
         }
      });
   },

   /**
    * Create and show dialog to add a buddy
    *
    * @param {string} [username] jabber id
    */
   showContactDialog: function(username) {
      jsxc.gui.dialog.open(jsxc.gui.template.get('contactDialog'));

      // If we got a friendship request, we would display the username in our
      // response
      if (username) {
         $('#jsxc_username').val(username);
      }

      $('#jsxc_username').keyup(function() {
         if (typeof jsxc.options.getUsers === 'function') {
            var val = $(this).val();
            $('#jsxc_userlist').empty();

            if (val !== '') {
               jsxc.options.getUsers.call(this, val, function(list) {
                  $('#jsxc_userlist').empty();
                  $.each(list || {}, function(uid, displayname) {
                     var option = $('<option>');
                     option.attr('data-username', uid);
                     option.attr('data-alias', displayname);

                     option.attr('value', uid).appendTo('#jsxc_userlist');

                     if (uid !== displayname) {
                        option.clone().attr('value', displayname).appendTo('#jsxc_userlist');
                     }
                  });
               });
            }
         }
      });

      $('#jsxc_username').on('input', function() {
         var val = $(this).val();
         var option = $('#jsxc_userlist').find('option[data-username="' + val + '"], option[data-alias="' + val + '"]');

         if (option.length > 0) {
            $('#jsxc_username').val(option.attr('data-username'));
            $('#jsxc_alias').val(option.attr('data-alias'));
         }
      });

      $('#jsxc_dialog form').submit(function(ev) {
         ev.preventDefault();

         var username = $('#jsxc_username').val();
         var alias = $('#jsxc_alias').val();

         if (!username.match(/@(.*)$/)) {
            username += '@' + Strophe.getDomainFromJid(jsxc.storage.getItem('jid'));
         }

         // Check if the username is valid
         if (!username || !username.match(jsxc.CONST.REGEX.JID)) {
            // Add notification
            $('#jsxc_username').addClass('jsxc_invalid').keyup(function() {
               if ($(this).val().match(jsxc.CONST.REGEX.JID)) {
                  $(this).removeClass('jsxc_invalid');
               }
            });
            return false;
         }
         jsxc.xmpp.addBuddy(username, alias);

         jsxc.gui.dialog.close();

         return false;
      });
   },

   /**
    * Create and show dialog to remove a buddy
    *
    * @param {type} bid
    * @returns {undefined}
    */
   showRemoveDialog: function(bid) {

      jsxc.gui.dialog.open(jsxc.gui.template.get('removeDialog', bid));

      var data = jsxc.storage.getUserItem('buddy', bid);

      $('#jsxc_dialog .jsxc_remove').click(function(ev) {
         ev.stopPropagation();

         if (jsxc.master) {
            jsxc.xmpp.removeBuddy(data.jid);
         } else {
            // inform master
            jsxc.storage.setUserItem('deletebuddy', bid, {
               jid: data.jid
            });
         }

         jsxc.gui.dialog.close();
      });
   },

   /**
    * Create and show a wait dialog
    *
    * @param {type} msg message to display to the user
    * @returns {undefined}
    */
   showWaitAlert: function(msg) {
      jsxc.gui.dialog.open(jsxc.gui.template.get('waitAlert', null, msg), {
         'noClose': true
      });
   },

   /**
    * Create and show a wait dialog
    *
    * @param {type} msg message to display to the user
    * @returns {undefined}
    */
   showAlert: function(msg) {
      jsxc.gui.dialog.open(jsxc.gui.template.get('alert', null, msg));
   },

   /**
    * Create and show a auth fail dialog
    *
    * @returns {undefined}
    */
   showAuthFail: function() {
      jsxc.gui.dialog.open(jsxc.gui.template.get('authFailDialog'));

      if (jsxc.options.loginForm.triggered !== false) {
         $('#jsxc_dialog .jsxc_cancel').hide();
      }

      $('#jsxc_dialog .jsxc_retry').click(function() {
         jsxc.gui.dialog.close();
      });

      $('#jsxc_dialog .jsxc_cancel').click(function() {
         jsxc.submitLoginForm();
      });
   },

   /**
    * Create and show a confirm dialog
    *
    * @param {String} msg Message
    * @param {function} confirm
    * @param {function} dismiss
    * @returns {undefined}
    */
   showConfirmDialog: function(msg, confirm, dismiss) {
      jsxc.gui.dialog.open(jsxc.gui.template.get('confirmDialog', null, msg), {
         noClose: true
      });

      if (confirm) {
         $('#jsxc_dialog .jsxc_confirm').click(confirm);
      }

      if (dismiss) {
         $('#jsxc_dialog .jsxc_dismiss').click(dismiss);
      }
   },

   /**
    * Show about dialog.
    *
    * @memberOf jsxc.gui
    */
   showAboutDialog: function() {
      jsxc.gui.dialog.open(jsxc.gui.template.get('aboutDialog'));

      $('#jsxc_dialog .jsxc_debuglog').click(function() {
         jsxc.gui.showDebugLog();
      });
   },

   /**
    * Show debug log.
    *
    * @memberOf jsxc.gui
    */
   showDebugLog: function() {
      var userInfo = '<h3>User information</h3>';

      if (navigator) {
         var key;
         for (key in navigator) {
            if (typeof navigator[key] === 'string') {
               userInfo += '<b>' + key + ':</b> ' + navigator[key] + '<br />';
            }
         }
      }

      if ($.fn && $.fn.jquery) {
         userInfo += '<b>jQuery:</b> ' + $.fn.jquery + '<br />';
      }

      if (window.screen) {
         userInfo += '<b>Height:</b> ' + window.screen.height + '<br />';
         userInfo += '<b>Width:</b> ' + window.screen.width + '<br />';
      }

      userInfo += '<b>jsxc version:</b> ' + jsxc.version + '<br />';

      jsxc.gui.dialog.open('<div class="jsxc_log">' + userInfo + '<h3>Log</h3><pre>' + jsxc.escapeHTML(jsxc.log) + '</pre></div>');
   },

   /**
    * Show vCard of user with the given bar jid.
    *
    * @memberOf jsxc.gui
    * @param {String} jid
    */
   showVcard: function(jid) {
      var bid = jsxc.jidToBid(jid);
      jsxc.gui.dialog.open(jsxc.gui.template.get('vCard', bid));

      var data = jsxc.storage.getUserItem('buddy', bid);

      if (data && data.res) {
         // Display resources and corresponding information
         var i, j, res, identities, identity = null,
            cap, client;
         for (i = 0; i < data.res.length; i++) {
            res = data.res[i];

            identities = [];
            cap = jsxc.xmpp.getCapabilitiesByJid(bid + '/' + res);

            if (cap !== null && cap.identities !== null) {
               identities = cap.identities;
            }

            client = '';
            for (j = 0; j < identities.length; j++) {
               identity = identities[j];
               if (identity.category === 'client') {
                  if (client !== '') {
                     client += ',\n';
                  }

                  client += identity.name + ' (' + identity.type + ')';
               }
            }

            var status = jsxc.storage.getUserItem('res', bid)[res];

            $('#jsxc_dialog ul.jsxc_vCard').append('<li class="jsxc_sep"><strong>' + $.t('Resource') + ':</strong> ' + res + '</li>');
            $('#jsxc_dialog ul.jsxc_vCard').append('<li><strong>' + $.t('Client') + ':</strong> ' + client + '</li>');
            $('#jsxc_dialog ul.jsxc_vCard').append('<li><strong>' + $.t('Status') + ':</strong> ' + $.t(jsxc.CONST.STATUS[status]) + '</li>');
         }
      }

      var printProp = function(el, depth) {
         var content = '';

         el.each(function() {
            var item = $(this);
            var children = $(this).children();

            content += '<li>';

            var prop = $.t(item[0].tagName);

            if (prop !== ' ') {
               content += '<strong>' + prop + ':</strong> ';
            }

            if (item[0].tagName === 'PHOTO') {

            } else if (children.length > 0) {
               content += '<ul>';
               content += printProp(children, depth + 1);
               content += '</ul>';
            } else if (item.text() !== '') {
               content += jsxc.escapeHTML(item.text());
            }

            content += '</li>';

            if (depth === 0 && $('#jsxc_dialog ul.jsxc_vCard').length > 0) {
               if ($('#jsxc_dialog ul.jsxc_vCard li.jsxc_sep:first').length > 0) {
                  $('#jsxc_dialog ul.jsxc_vCard li.jsxc_sep:first').before(content);
               } else {
                  $('#jsxc_dialog ul.jsxc_vCard').append(content);
               }
               content = '';
            }
         });

         if (depth > 0) {
            return content;
         }
      };

      var failedToLoad = function() {
         if ($('#jsxc_dialog ul.jsxc_vCard').length === 0) {
            return;
         }

         $('#jsxc_dialog p').remove();

         var content = '<p>';
         content += $.t('Sorry_your_buddy_doesnt_provide_any_information');
         content += '</p>';

         $('#jsxc_dialog').append(content);
      };

      jsxc.xmpp.loadVcard(bid, function(stanza) {

         if ($('#jsxc_dialog ul.jsxc_vCard').length === 0) {
            return;
         }

         $('#jsxc_dialog p').remove();

         var photo = $(stanza).find("vCard > PHOTO");

         if (photo.length > 0) {
            var img = photo.find('BINVAL').text();
            var type = photo.find('TYPE').text();
            var src = 'data:' + type + ';base64,' + img;

            if (photo.find('EXTVAL').length > 0) {
               src = photo.find('EXTVAL').text();
            }

            // concat chunks
            src = src.replace(/[\t\r\n\f]/gi, '');

            var img_el = $('<img class="jsxc_vCard" alt="avatar" />');
            img_el.attr('src', src);

            $('#jsxc_dialog h3').before(img_el);
         }

         if ($(stanza).find('vCard').length === 0 || ($(stanza).find('vcard > *').length === 1 && photo.length === 1)) {
            failedToLoad();
            return;
         }

         printProp($(stanza).find('vcard > *'), 0);

      }, failedToLoad);
   },

   showSettings: function() {
      jsxc.gui.dialog.open(jsxc.gui.template.get('settings'));

      if (jsxc.options.get('xmpp').overwrite === 'false' || jsxc.options.get('xmpp').overwrite === false) {
         $('.jsxc_fieldsetXmpp').parent().hide();
      }

      $('#jsxc_dialog form').each(function() {
         var self = $(this);

         self.find('input[type!="submit"]').each(function() {
            var id = this.id.split("-");
            var prop = id[0];
            var key = id[1];
            var type = this.type;

            var data = jsxc.options.get(prop);

            if (data && typeof data[key] !== 'undefined') {
               if (type === 'checkbox') {
                  if (data[key] !== 'false' && data[key] !== false) {
                     this.checked = 'checked';
                  }
               } else {
                  $(this).val(data[key]);
               }
            }
         });
      });

      $('#jsxc_dialog form').submit(function() {

         var self = $(this);
         var data = {};

         self.find('input[type!="submit"]').each(function() {
            var id = this.id.split("-");
            var prop = id[0];
            var key = id[1];
            var val;
            var type = this.type;

            if (type === 'checkbox') {
               val = this.checked;
            } else {
               val = $(this).val();
            }

            if (!data[prop]) {
               data[prop] = {};
            }

            data[prop][key] = val;
         });

         $.each(data, function(key, val) {
            jsxc.options.set(key, val);
         });

         var cb = function(success) {
            if (typeof self.attr('data-onsubmit') === 'string') {
               jsxc.exec(self.attr('data-onsubmit'), [success]);
            }

            setTimeout(function() {
               if (success) {
                  self.find('button[type="submit"]').switchClass('btn-primary', 'btn-success');
               } else {
                  self.find('button[type="submit"]').switchClass('btn-primary', 'btn-danger');
               }
               setTimeout(function() {
                  self.find('button[type="submit"]').switchClass('btn-danger btn-success', 'btn-primary');
               }, 2000);
            }, 200);
         };

         jsxc.options.saveSettinsPermanent.call(this, data, cb);

         return false;
      });
   },

   /**
    * Show prompt for notification permission.
    *
    * @memberOf jsxc.gui
    */
   showRequestNotification: function() {

      jsxc.switchEvents({
         'notificationready.jsxc': function() {
            jsxc.gui.dialog.close();
            jsxc.notification.init();
            jsxc.storage.setUserItem('notification', 1);
         },
         'notificationfailure.jsxc': function() {
            jsxc.gui.dialog.close();
            jsxc.options.notification = false;
            jsxc.storage.setUserItem('notification', 0);
         }
      });

      jsxc.gui.showConfirmDialog($.t('Should_we_notify_you_'), function() {
         jsxc.gui.dialog.open(jsxc.gui.template.get('pleaseAccept'), {
            noClose: true
         });

         jsxc.notification.requestPermission();
      }, function() {
         $(document).trigger('notificationfailure.jsxc');
      });
   },

   showUnknownSender: function(bid) {
      var confirmationText = $.t('You_received_a_message_from_an_unknown_sender_', {
         sender: bid
      });
      jsxc.gui.showConfirmDialog(confirmationText, function() {

         jsxc.gui.dialog.close();

         jsxc.storage.saveBuddy(bid, {
            jid: bid,
            name: bid,
            status: 0,
            sub: 'none',
            res: []
         });

         jsxc.gui.window.open(bid);

      }, function() {
         // reset state
         jsxc.storage.removeUserItem('chat', bid);
      });
   },

   showSelectionDialog: function(header, msg, primary, option, primaryLabel, optionLabel) {
      var opt;

      if (arguments.length === 1 && typeof header === 'object' && header !== null) {
         opt = header;
      } else {
         opt = {
            header: header,
            msg: msg,
            primary: {
               label: primaryLabel,
               cb: primary
            },
            option: {
               label: optionLabel,
               cb: option
            }
         };
      }

      var dialog = jsxc.gui.dialog.open(jsxc.gui.template.get('selectionDialog'), {
         noClose: true
      });

      if (opt.header) {
         dialog.find('h3').text(opt.header);
      } else {
         dialog.find('h3').hide();
      }

      if (opt.msg) {
         dialog.find('p').text(opt.msg);
      } else {
         dialog.find('p').hide();
      }

      if (opt.primary && opt.primary.label) {
         dialog.find('.btn-primary').text(opt.primary.label);
      }

      if (opt.primary && opt.option.label) {
         dialog.find('.btn-default').text(opt.option.label);
      }

      if (opt.primary && opt.primary.cb) {
         dialog.find('.btn-primary').click(opt.primary.cb);
      }

      if (opt.primary && opt.option.cb) {
         dialog.find('.btn-primary').click(opt.option.cb);
      }
   },

   /**
    * Show notification dialog.
    *
    * @param  {String} subject
    * @param  {String} body
    * @param  {String} from
    */
   showNotification: function(subject, body, from) {
      var dialog = jsxc.gui.dialog.open(jsxc.gui.template.get('notification'));

      dialog.find('h3').text(subject);
      dialog.find('.jsxc_msg').text(body);

      if (from) {
         dialog.find('.jsxc_meta').text($.t('from') + ' ' + from);
      } else {
         dialog.find('.jsxc_meta').hide();
      }
   },

   /**
    * Change own presence to pres.
    *
    * @memberOf jsxc.gui
    * @param pres {CONST.STATUS} New presence state
    * @param external {boolean} True if triggered from other tab.
    */
   changePresence: function(pres, external) {

      if (external !== true) {
         jsxc.storage.setUserItem('presence', pres);
      }

      if (jsxc.master) {
         jsxc.xmpp.sendPres();
      }

      $('#jsxc_presence > span').text($('#jsxc_presence .jsxc_inner ul .jsxc_' + pres).text());

      jsxc.gui.updatePresence('own', pres);
   },

   /**
    * Update all presence objects for given user.
    *
    * @memberOf jsxc.gui
    * @param bid bar jid of user.
    * @param {CONST.STATUS} pres New presence state.
    */
   updatePresence: function(bid, pres) {

      if (bid === 'own') {
         if (pres === 'dnd') {
            $('#jsxc_menu .jsxc_muteNotification').addClass('jsxc_disabled');
            jsxc.notification.muteSound(true);
         } else {
            $('#jsxc_menu .jsxc_muteNotification').removeClass('jsxc_disabled');

            if (!jsxc.options.get('muteNotification')) {
               jsxc.notification.unmuteSound(true);
            }
         }
      }

      $('[data-bid="' + bid + '"]').each(function() {
         var el = $(this);

         el.attr('data-status', pres);

         if (!el.hasClass('jsxc_statusIndicator')) {
            el = el.find('.jsxc_statusIndicator');
         }

         el.attr('data-status', pres);

         el.removeClass('jsxc_' + jsxc.CONST.STATUS.join(' jsxc_')).addClass('jsxc_' + pres);
      });
   },

   /**
    * Switch read state to UNread and increase counter.
    *
    * @memberOf jsxc.gui
    * @param bid
    */
   unreadMsg: function(bid) {
      var winData = jsxc.storage.getUserItem('window', bid) || {};
      var count = (winData && winData.unread) || 0;
      count = (count === true) ? 1 : count + 1; //unread was boolean (<2.1.0)

      // update user counter
      winData.unread = count;
      jsxc.storage.setUserItem('window', bid, winData);

      // update counter of total unread messages
      var total = jsxc.storage.getUserItem('unreadMsg') || 0;
      total++;
      jsxc.storage.setUserItem('unreadMsg', total);

      if (jsxc.gui.favicon) {
         jsxc.gui.favicon.badge(total);
      }

      jsxc.gui._unreadMsg(bid, count);
   },

   /**
    * Switch read state to UNread.
    *
    * @memberOf jsxc.gui
    * @param bid
    * @param count
    */
   _unreadMsg: function(bid, count) {
      var win = jsxc.gui.window.get(bid);

      if (typeof count !== 'number') {
         // get counter after page reload
         var winData = jsxc.storage.getUserItem('window', bid);
         count = (winData && winData.unread) || 1;
         count = (count === true) ? 1 : count; //unread was boolean (<2.1.0)
      }

      var el = jsxc.gui.roster.getItem(bid).add(win);

      el.addClass('jsxc_unreadMsg');
      el.find('.jsxc_unread').text(count);
   },

   /**
    * Switch read state to read.
    *
    * @memberOf jsxc.gui
    * @param bid
    */
   readMsg: function(bid) {
      var win = jsxc.gui.window.get(bid);
      var winData = jsxc.storage.getUserItem('window', bid);
      var count = (winData && winData.unread) || 0;
      count = (count === true) ? 0 : count; //unread was boolean (<2.1.0)

      var el = jsxc.gui.roster.getItem(bid).add(win);
      el.removeClass('jsxc_unreadMsg');
      el.find('.jsxc_unread').text(0);

      // update counters if not called from other tab
      if (count > 0) {
         // update counter of total unread messages
         var total = jsxc.storage.getUserItem('unreadMsg') || 0;
         total -= count;
         jsxc.storage.setUserItem('unreadMsg', total);

         if (jsxc.gui.favicon) {
            jsxc.gui.favicon.badge(total);
         }

         jsxc.storage.updateUserItem('window', bid, 'unread', 0);
      }
   },

   /**
    * This function searches for URI scheme according to XEP-0147.
    *
    * @memberOf jsxc.gui
    * @param container In which element should we search?
    */
   detectUriScheme: function(container) {
      container = (container) ? $(container) : $('body');

      container.find("a[href^='xmpp:']").each(function() {

         var element = $(this);
         var href = element.attr('href').replace(/^xmpp:/, '');
         var jid = href.split('?')[0];
         var action, params = {};

         element.attr('data-bid', jsxc.jidToBid(jid));
         jsxc.gui.update(jsxc.jidToBid(jid));

         if (href.indexOf('?') < 0) {
            action = 'message';
         } else {
            var pairs = href.substring(href.indexOf('?') + 1).split(';');
            action = pairs[0];

            var i, key, value;
            for (i = 1; i < pairs.length; i++) {
               key = pairs[i].split('=')[0];
               value = (pairs[i].indexOf('=') > 0) ? pairs[i].substring(pairs[i].indexOf('=') + 1) : null;

               params[decodeURIComponent(key)] = decodeURIComponent(value);
            }
         }

         if (typeof jsxc.gui.queryActions[action] === 'function') {
            element.addClass('jsxc_uriScheme jsxc_uriScheme_' + action);

            element.off('click').click(function(ev) {
               ev.stopPropagation();

               if (jsxc.xmpp.conn && jsxc.xmpp.conn.connected) {
                  jsxc.gui.queryActions[action].call(jsxc, jid, params);
               } else {
                  jsxc.gui.showNotification($.t('no_connection'), $.t('You_have_to_go_online_'));
               }

               return false;
            });
         }
      });
   },

   detectEmail: function(container) {
      container = (container) ? $(container) : $('body');

      container.find('a[href^="mailto:"],a[href^="xmpp:"]').each(function() {
         var spot = $("<span>X</span>").addClass("jsxc_spot");
         var href = $(this).attr("href").replace(/^ *(mailto|xmpp):/, "").trim();

         if (href !== '' && href !== Strophe.getBareJidFromJid(jsxc.storage.getItem("jid"))) {
            var bid = jsxc.jidToBid(href);
            var self = $(this);
            var s = self.prev();

            if (!s.hasClass('jsxc_spot')) {
               s = spot.clone().attr('data-bid', bid);

               self.before(s);
            }

            s.off('click');

            if (jsxc.storage.getUserItem('buddy', bid)) {
               jsxc.gui.update(bid);
               s.click(function() {
                  jsxc.gui.window.open(bid);

                  return false;
               });
            } else {
               s.click(function() {
                  jsxc.gui.showContactDialog(href);

                  return false;
               });
            }
         }
      });
   },

   avatarPlaceholder: function(el, seed, text) {
      text = text || seed;

      var options = jsxc.options.get('avatarplaceholder') || {};
      var hash = jsxc.hashStr(seed);

      var hue = Math.abs(hash) % 360;
      var saturation = options.saturation || 90;
      var lightness = options.lightness || 65;

      el.css({
         'background-color': 'hsl(' + hue + ', ' + saturation + '%, ' + lightness + '%)',
         'color': '#fff',
         'font-weight': 'bold',
         'text-align': 'center',
         'line-height': el.height() + 'px',
         'font-size': el.height() * 0.6 + 'px'
      });

      if (typeof text === 'string' && text.length > 0) {
         el.text(text[0].toUpperCase());
      }
   },

   /**
    * Replace shortname emoticons with images.
    *
    * @param  {string} str text with emoticons as shortname
    * @return {string} text with emoticons as images
    */
   shortnameToImage: function(str) {
      str = str.replace(jsxc.gui.regShortNames, function(shortname) {
         if (typeof shortname === 'undefined' || shortname === '' || (!(shortname in jsxc.gui.emoticonList.emojione) && !(shortname in jsxc.gui.emoticonList.core))) {
            return shortname;
         }

         var src, filename;

         if (jsxc.gui.emoticonList.core[shortname]) {
            filename = jsxc.gui.emoticonList.core[shortname][jsxc.gui.emoticonList.core[shortname].length - 1].replace(/^:([^:]+):$/, '$1');
            src = jsxc.options.root + '/img/emotions/' + filename + '.svg';
         } else if (jsxc.gui.emoticonList.emojione[shortname]) {
            filename = jsxc.gui.emoticonList.emojione[shortname].fname;
            src = jsxc.options.root + '/lib/emojione/assets/svg/' + filename + '.svg';
         }

         var div = $('<div>');

         div.addClass('jsxc_emoticon');
         div.css('background-image', 'url(' + src + ')');
         div.attr('title', shortname);

         return div.prop('outerHTML');
      });

      var obj = $('<div>' + str + '</div>');
      if (obj.find('.jsxc_emoticon').length === 1 && obj.text().replace(/ /, '').length === 0 && obj.find('*').length === 1) {
         obj.find('.jsxc_emoticon').addClass('jsxc_large');
         str = obj.prop('outerHTML');
      }

      return str;
   },

   restore: function() {
      jsxc.restoreRoster();
      jsxc.restoreWindows();
      jsxc.restoreCompleted = true;

      $(document).trigger('restoreCompleted.jsxc');
      jsxc.changeUIState(jsxc.CONST.UISTATE.READY);
   }
};

/**
 * Handle functions related to the gui of the roster
 *
 * @namespace jsxc.gui.roster
 */
jsxc.gui.roster = {

   /** True if roster is initialised */
   ready: false,

   /** True if all items are loaded */
   loaded: false,

   /**
    * Init the roster skeleton
    *
    * @memberOf jsxc.gui.roster
    * @returns {undefined}
    */
   init: function() {
      $(jsxc.options.rosterAppend + ':first').append($(jsxc.gui.template.get('roster')));

      if (jsxc.options.get('hideOffline')) {
         $('#jsxc_menu .jsxc_hideOffline').text($.t('Show_offline'));
         $('#jsxc_buddylist').addClass('jsxc_hideOffline');
      }

      $('#jsxc_menu .jsxc_settings').click(function() {
         jsxc.gui.showSettings();
      });

      $('#jsxc_menu .jsxc_hideOffline').click(function() {
         var hideOffline = !jsxc.options.get('hideOffline');

         if (hideOffline) {
            $('#jsxc_buddylist').addClass('jsxc_hideOffline');
         } else {
            $('#jsxc_buddylist').removeClass('jsxc_hideOffline');
         }

         $(this).text(hideOffline ? $.t('Show_offline') : $.t('Hide_offline'));

         jsxc.options.set('hideOffline', hideOffline);
      });

      if (jsxc.options.get('muteNotification')) {
         jsxc.notification.muteSound();
      }

      $('#jsxc_menu .jsxc_muteNotification').click(function() {

         if (jsxc.storage.getUserItem('presence') === 'dnd') {
            return;
         }

         // invert current choice
         var mute = !jsxc.options.get('muteNotification');

         if (mute) {
            jsxc.notification.muteSound();
         } else {
            jsxc.notification.unmuteSound();
         }
      });

      $('#jsxc_roster .jsxc_addBuddy').click(function() {
         jsxc.gui.showContactDialog();
      });

      $('#jsxc_roster .jsxc_onlineHelp').click(function() {
         window.open(jsxc.options.onlineHelp, 'onlineHelp');
      });

      $('#jsxc_roster .jsxc_about').click(function() {
         jsxc.gui.showAboutDialog();
      });

      $('#jsxc_toggleRoster').click(function() {
         jsxc.gui.roster.toggle();
      });

      $('#jsxc_presence li').click(function() {
         var self = $(this);
         var pres = self.data('pres');

         if (pres === 'offline') {
            jsxc.xmpp.logout(false);
         } else {
            jsxc.gui.changePresence(pres);
         }
      });

      $('#jsxc_buddylist').slimScroll({
         distance: '3px',
         height: ($('#jsxc_roster').height() - 31) + 'px',
         width: $('#jsxc_buddylist').width() + 'px',
         color: '#fff',
         opacity: '0.5'
      });

      $('#jsxc_roster > .jsxc_bottom > div').each(function() {
         jsxc.gui.toggleList.call($(this));
      });

      var rosterState = jsxc.storage.getUserItem('roster') || (jsxc.options.get('loginForm').startMinimized ? 'hidden' : 'shown');

      $('#jsxc_roster').addClass('jsxc_state_' + rosterState);
      $('#jsxc_windowList').addClass('jsxc_roster_' + rosterState);

      var pres = jsxc.storage.getUserItem('presence') || 'online';
      $('#jsxc_presence > span').text($('#jsxc_presence .jsxc_' + pres).text());
      jsxc.gui.updatePresence('own', pres);

      jsxc.gui.tooltip('#jsxc_roster');

      jsxc.notice.load();

      jsxc.gui.roster.ready = true;
      $(document).trigger('ready.roster.jsxc', [rosterState]);
      $(document).trigger('ready-roster-jsxc', [rosterState]);
   },

   /**
    * Create roster item and add it to the roster
    *
    * @param {String} bid bar jid
    */
   add: function(bid) {
      var data = jsxc.storage.getUserItem('buddy', bid);
      var bud = jsxc.gui.buddyTemplate.clone().attr('data-bid', bid).attr('data-type', data.type || 'chat');

      // remove all messages (offline, empty roster) from roster
      $('#jsxc_roster > p').remove();

      jsxc.gui.roster.insert(bid, bud);

      bud.click(function() {
         jsxc.gui.window.open(bid);
      });

      bud.find('.jsxc_msg').click(function() {
         jsxc.gui.window.open(bid);

         return false;
      });

      bud.find('.jsxc_rename').click(function() {
         jsxc.gui.roster.rename(bid);
         return false;
      });

      if (data.type !== 'groupchat') {
         bud.find('.jsxc_delete').click(function() {
            jsxc.gui.showRemoveDialog(bid);
            return false;
         });
      }

      var expandClick = function() {
         bud.trigger('extra.jsxc');

         $('body').click();

         if (!bud.find('.jsxc_menu').hasClass('jsxc_open')) {
            bud.find('.jsxc_menu').addClass('jsxc_open');

            $('body').one('click', function() {
               bud.find('.jsxc_menu').removeClass('jsxc_open');
            });
         }

         return false;
      };

      bud.find('.jsxc_more').click(expandClick);

      bud.find('.jsxc_vcard').click(function() {
         jsxc.gui.showVcard(data.jid);

         return false;
      });

      jsxc.gui.update(bid);

      // update scrollbar
      $('#jsxc_buddylist').slimScroll({
         scrollTo: '0px'
      });

      var history = jsxc.storage.getUserItem('history', bid) || [];
      var i = 0;
      while (history.length > i) {
         var message = new jsxc.Message(history[i]);
         if (message.direction !== jsxc.Message.SYS) {
            jsxc.gui.window.setLastMsg(bid, message.msg);
            break;
         }
         i++;
      }

      $(document).trigger('add.roster.jsxc', [bid, data, bud]);
   },

   getItem: function(bid) {
      return $("#jsxc_buddylist > li[data-bid='" + bid + "']");
   },

   /**
    * Insert roster item. First order: online > away > offline. Second order:
    * alphabetical of the name
    *
    * @param {type} bid
    * @param {jquery} li roster item which should be insert
    * @returns {undefined}
    */
   insert: function(bid, li) {

      var data = jsxc.storage.getUserItem('buddy', bid);
      var listElements = $('#jsxc_buddylist > li');
      var insert = false;

      if (!data.name) {
         data.name = bid;
      }

      // Insert buddy with no mutual friendship to the end
      var status = (data.sub === 'both') ? data.status : -1;

      listElements.each(function() {

         var thisStatus = ($(this).data('sub') === 'both') ? $(this).data('status') : -1;

         if (($(this).data('name').toLowerCase() > data.name.toLowerCase() && thisStatus === status) || thisStatus < status) {

            $(this).before(li);
            insert = true;

            return false;
         }
      });

      if (!insert) {
         li.appendTo('#jsxc_buddylist');
      }
   },

   /**
    * Initiate reorder of roster item
    *
    * @param {type} bid
    * @returns {undefined}
    */
   reorder: function(bid) {
      jsxc.gui.roster.insert(bid, jsxc.gui.roster.remove(bid));
   },

   /**
    * Removes buddy from roster
    *
    * @param {String} bid bar jid
    * @return {JQueryObject} Roster list element
    */
   remove: function(bid) {
      return jsxc.gui.roster.getItem(bid).detach();
   },

   /**
    * Removes buddy from roster and clean up
    *
    * @param {String} bid bar compatible jid
    */
   purge: function(bid) {
      if (jsxc.master) {
         jsxc.storage.removeUserItem('buddy', bid);
         jsxc.storage.removeUserItem('otr', bid);
         jsxc.storage.removeUserItem('otr_version_' + bid);
         jsxc.storage.removeUserItem('chat', bid);
         jsxc.storage.removeUserItem('window', bid);
         jsxc.storage.removeUserElement('buddylist', bid);
         jsxc.storage.removeUserElement('windowlist', bid);
      }

      jsxc.gui.window._close(bid);
      jsxc.gui.roster.remove(bid);
   },

   /**
    * Create input element for rename action
    *
    * @param {type} bid
    * @returns {undefined}
    */
   rename: function(bid) {
      var name = jsxc.gui.roster.getItem(bid).find('.jsxc_name');
      var options = jsxc.gui.roster.getItem(bid).find('.jsxc_lastmsg, .jsxc_more');
      var input = $('<input type="text" name="name"/>');

      // hide more menu
      $('body').click();

      options.hide();
      name = name.replaceWith(input);

      input.val(name.text());
      input.keypress(function(ev) {
         if (ev.which !== 13) {
            return;
         }

         options.css('display', '');
         input.replaceWith(name);
         jsxc.gui.roster._rename(bid, $(this).val());

         $('html').off('click');
      });

      // Disable html click event, if click on input
      input.click(function() {
         return false;
      });

      $('html').one('click', function() {
         options.css('display', '');
         input.replaceWith(name);
         jsxc.gui.roster._rename(bid, input.val());
      });
   },

   /**
    * Rename buddy
    *
    * @param {type} bid
    * @param {type} newname new name of buddy
    * @returns {undefined}
    */
   _rename: function(bid, newname) {
      if (jsxc.master) {
         var d = jsxc.storage.getUserItem('buddy', bid) || {};

         if (d.type === 'chat') {
            var iq = $iq({
               type: 'set'
            }).c('query', {
               xmlns: 'jabber:iq:roster'
            }).c('item', {
               jid: Strophe.getBareJidFromJid(d.jid),
               name: newname
            });
            jsxc.xmpp.conn.sendIQ(iq);
         } else if (d.type === 'groupchat') {
            jsxc.xmpp.bookmarks.add(bid, newname, d.nickname, d.autojoin);
         }
      }

      jsxc.storage.updateUserItem('buddy', bid, 'name', newname);
      jsxc.gui.update(bid);
   },

   /**
    * Toogle complete roster
    *
    * @param {string} state Toggle to state
    */
   toggle: function(state) {
      var duration;

      var roster = $('#jsxc_roster');
      var wl = $('#jsxc_windowList');

      if (!state) {
         state = (jsxc.storage.getUserItem('roster') === jsxc.CONST.HIDDEN) ? jsxc.CONST.SHOWN : jsxc.CONST.HIDDEN;
      }

      if (state === 'shown' && jsxc.isExtraSmallDevice()) {
         jsxc.gui.window.hide();
      }

      jsxc.storage.setUserItem('roster', state);

      roster.removeClass('jsxc_state_hidden jsxc_state_shown').addClass('jsxc_state_' + state);
      wl.removeClass('jsxc_roster_hidden jsxc_roster_shown').addClass('jsxc_roster_' + state);

      duration = parseFloat(roster.css('transitionDuration') || 0) * 1000;

      setTimeout(function() {
         jsxc.gui.updateWindowListSB();
      }, duration);

      $(document).trigger('toggle.roster.jsxc', [state, duration]);

      return duration;
   },

   /**
    * Shows a text with link to a login box that no connection exists.
    */
   noConnection: function() {
      $('#jsxc_roster').addClass('jsxc_noConnection');

      $('#jsxc_buddylist').empty();

      $('#jsxc_roster').append($('<p>' + $.t('no_connection') + '</p>').append(' <a>' + $.t('relogin') + '</a>').click(function() {
         jsxc.gui.showLoginBox();
      }));
   },

   /**
    * Shows a text with link to add a new buddy.
    *
    * @memberOf jsxc.gui.roster
    */
   empty: function() {
      var text = $('<p>' + $.t('Your_roster_is_empty_add_') + '</p>');
      var link = text.find('a');

      link.click(function() {
         jsxc.gui.showContactDialog();
      });
      text.append(link);
      text.append('.');

      $('#jsxc_roster').prepend(text);
   }
};

/**
 * Wrapper for dialog
 *
 * @namespace jsxc.gui.dialog
 */
jsxc.gui.dialog = {
   /**
    * Open a Dialog.
    *
    * @memberOf jsxc.gui.dialog
    * @param {String} data Data of the dialog
    * @param {Object} [o] Options for the dialog
    * @param {Boolean} [o.noClose] If true, hide all default close options
    * @returns {jQuery} Dialog object
    */
   open: function(data, o) {

      var opt = $.extend({
         name: ''
      }, o);

      var src = $('<div data-name="' + opt.name + '" id="jsxc_dialog" />').append(data);

      $.magnificPopup.open({
         items: {
            src: src
         },
         type: 'inline',
         modal: opt.noClose,
         callbacks: {
            beforeClose: function() {
               $(document).trigger('cleanup.dialog.jsxc');
            },
            afterClose: function() {
               $(document).trigger('close.dialog.jsxc');
            },
            open: function() {
               $('#jsxc_dialog .jsxc_close').click(function(ev) {
                  ev.preventDefault();

                  jsxc.gui.dialog.close();
               });

               $('#jsxc_dialog form').each(function() {
                  var form = $(this);

                  form.find('button[data-jsxc-loading-text]').each(function() {
                     var btn = $(this);

                     btn.on('btnloading.jsxc', function() {
                        if (!btn.prop('disabled')) {
                           btn.prop('disabled', true);

                           btn.data('jsxc_value', btn.text());

                           btn.text(btn.attr('data-jsxc-loading-text'));
                        }
                     });

                     btn.on('btnfinished.jsxc', function() {
                        if (btn.prop('disabled')) {
                           btn.prop('disabled', false);

                           btn.text(btn.data('jsxc_value'));
                        }
                     });
                  });
               });

               jsxc.gui.dialog.resize();

               $(document).trigger('complete.dialog.jsxc');
            }
         }
      });

      return $('#jsxc_dialog');
   },

   /**
    * If no name is provided every dialog will be closed,
    * otherwise only dialog with given name is closed.
    *
    * @param {string} [name] Close only dialog with the given name
    */
   close: function(name) {
      jsxc.debug('close dialog');

      if (typeof name === 'string' && name.length > 0 && !jsxc.el_exists('#jsxc_dialog[data-name=' + name + ']')) {
         return;
      }

      $.magnificPopup.close();
   },

   /**
    * Resizes current dialog.
    *
    * @param {Object} options e.g. width and height
    */
   resize: function() {

   }
};

/**
 * Handle functions related to the gui of the window
 *
 * @namespace jsxc.gui.window
 */
jsxc.gui.window = {
   /**
    * Init a window skeleton
    *
    * @memberOf jsxc.gui.window
    * @param {String} bid
    * @returns {jQuery} Window object
    */
   init: function(bid) {
      if (jsxc.gui.window.get(bid).length > 0) {
         return jsxc.gui.window.get(bid);
      }

      var win = jsxc.gui.windowTemplate.clone().attr('data-bid', bid).appendTo('#jsxc_windowList > ul');
      var data = jsxc.storage.getUserItem('buddy', bid);

      // Attach jid to window
      win.data('jid', data.jid);

      // Add handler

      // @TODO generalize this. Duplicate of jsxc.roster.add
      var expandClick = function() {
         win.trigger('extra.jsxc');

         $('body').click();

         if (!win.find('.jsxc_menu').hasClass('jsxc_open')) {
            win.find('.jsxc_menu').addClass('jsxc_open');

            $('body').one('click', function() {
               win.find('.jsxc_menu').removeClass('jsxc_open');
            });
         }

         return false;
      };

      win.find('.jsxc_more').click(expandClick);
      win.find('.jsxc_menu').click(function() {
         $('body').click();
      });

      win.find('.jsxc_verification').click(function() {
         jsxc.gui.showVerification(bid);
      });

      win.find('.jsxc_fingerprints').click(function() {
         jsxc.gui.showFingerprints(bid);
      });

      win.find('.jsxc_transfer').click(function() {
         jsxc.otr.toggleTransfer(bid);
      });

      win.find('.jsxc_bar').click(function() {
         jsxc.gui.window.toggle(bid);
      });

      win.find('.jsxc_close').click(function() {
         jsxc.gui.window.close(bid);
      });

      win.find('.jsxc_clear').click(function() {
         jsxc.gui.window.clear(bid);
      });

      win.find('.jsxc_sendFile').click(function() {
         $('body').click();

         if ($(this).hasClass('jsxc_disabled')) {
            return;
         }

         jsxc.gui.window.sendFile(bid);
      });

      win.find('.jsxc_tools').click(function() {
         return false;
      });

      var textinputBlurTimeout;
      win.find('.jsxc_textinput').keyup(function(ev) {
         var body = $(this).val();

         // I'm composing a message
         if (ev.which !== 13) {
            jsxc.xmpp.chatState.startComposing(bid);
         }

         if (ev.which === 13 && !ev.shiftKey) {
            body = '';

            jsxc.xmpp.chatState.endComposing(bid);
         }

         jsxc.storage.updateUserItem('window', bid, 'text', body);

         if (ev.which === 27) {
            jsxc.gui.window.close(bid);
         }
      }).keypress(function(ev) {
         if (ev.which !== 13 || ev.shiftKey || !$(this).val()) {
            resizeTextarea.call(this);
            return;
         }

         jsxc.gui.window.postMessage({
            bid: bid,
            direction: jsxc.Message.OUT,
            msg: $(this).val()
         });

         $(this).css('height', '').val('');

         ev.preventDefault();
      }).focus(function() {
         if (textinputBlurTimeout) {
            clearTimeout(textinputBlurTimeout);
         }

         // remove unread flag
         jsxc.gui.readMsg(bid);

         resizeTextarea.call(this);
      }).blur(function() {
         var self = $(this);

         textinputBlurTimeout = setTimeout(function() {
            self.css('height', '');
         }, 1200);
      }).mouseenter(function() {
         $('#jsxc_windowList').data('isOver', true);
      }).mouseleave(function() {
         $('#jsxc_windowList').data('isOver', false);
      });

      function resizeTextarea() {
         if (!$(this).data('originalHeight')) {
            $(this).data('originalHeight', $(this).outerHeight());
         }
         // compensate rounding error
         if ($(this).outerHeight() < (this.scrollHeight - 1) && $(this).val()) {
            $(this).height($(this).data('originalHeight') * 1.5);
         }
      }

      win.find('.jsxc_textarea').click(function() {
         // check if user clicks element or selects text
         if (typeof getSelection === 'function' && !getSelection().toString()) {
            win.find('.jsxc_textinput').focus();
         }
      });

      win.find('.jsxc_textarea').slimScroll({
         height: '234px',
         distance: '3px'
      });

      win.find('.jsxc_name').disableSelection();

      win.find('.slimScrollDiv').resizable({
         handles: 'w, nw, n',
         minHeight: 234,
         minWidth: 250,
         resize: function(event, ui) {
            jsxc.gui.window.resize(win, ui);
         },
         start: function() {
            win.removeClass('jsxc_normal');
         },
         stop: function() {
            win.addClass('jsxc_normal');
         }
      });

      win.find('.jsxc_window').css('bottom', -1 * win.find('.jsxc_fade').height());

      if ($.inArray(bid, jsxc.storage.getUserItem('windowlist')) < 0) {

         // add window to windowlist
         var wl = jsxc.storage.getUserItem('windowlist') || [];
         wl.push(bid);
         jsxc.storage.setUserItem('windowlist', wl);

         // init window element in storage
         jsxc.storage.setUserItem('window', bid, {
            minimize: true,
            text: '',
            unread: 0
         });

         jsxc.gui.window.hide(bid);
      } else {

         if (jsxc.storage.getUserItem('window', bid).unread) {
            jsxc.gui._unreadMsg(bid);
         }
      }

      $.each(jsxc.gui.emotions, function(i, val) {
         var ins = val[0].split(' ')[0];
         var li = $('<li>');
         li.append(jsxc.gui.shortnameToImage(':' + val[1] + ':'));
         li.find('div').attr('title', ins);
         li.click(function() {
            win.find('.jsxc_textinput').val(win.find('.jsxc_textinput').val() + ins);
            win.find('.jsxc_textinput').focus();
         });
         win.find('.jsxc_emoticons ul').prepend(li);
      });

      jsxc.gui.toggleList.call(win.find('.jsxc_emoticons'));

      jsxc.gui.window.restoreChat(bid);

      jsxc.gui.update(bid);

      jsxc.gui.updateWindowListSB();

      // create related otr object
      if (jsxc.master && !jsxc.otr.objects[bid]) {
         jsxc.otr.create(bid);
      } else {
         jsxc.otr.enable(bid);
      }

      $(document).trigger('init.window.jsxc', [win]);

      return win;
   },

   /**
    * Resize given window to given size. If no size is provided the window is resized to the default size.
    *
    * @param  {(string|jquery)} win Bid or window object
    * @param  {object} ui    The size has to be in the format {size:{width: [INT], height: [INT]}}
    * @param  {boolean} [outer] If true the given size is used as outer dimensions.
    */
   resize: function(win, ui, outer) {
      var bid;

      if (typeof win === 'object') {
         bid = win.attr('data-bid');
      } else if (typeof win === 'string') {
         bid = win;
         win = jsxc.gui.window.get(bid);
      } else {
         jsxc.warn('jsxc.gui.window.resize has to be called either with bid or window object.');
         return;
      }

      if (!win.attr('data-default-height')) {
         win.attr('data-default-height', win.find('.ui-resizable').height());
      }

      if (!win.attr('data-default-width')) {
         win.attr('data-default-width', win.find('.ui-resizable').width());
      }

      var outer_height_diff = (outer) ? win.find('.jsxc_window').outerHeight() - win.find('.ui-resizable').height() : 0;

      ui = $.extend({
         size: {
            width: parseInt(win.attr('data-default-width')),
            height: parseInt(win.attr('data-default-height')) + outer_height_diff
         }
      }, ui || {});

      if (outer) {
         ui.size.height -= outer_height_diff;
      }

      win.find('.slimScrollDiv').css({
         width: ui.size.width,
         height: ui.size.height
      });

      win.width(ui.size.width);

      win.find('.jsxc_textarea').slimScroll({
         height: ui.size.height
      });

      // var offset = win.find('.slimScrollDiv').position().top;
      //win.find('.jsxc_emoticons').css('top', (ui.size.height + offset + 6) + 'px');

      $(document).trigger('resize.window.jsxc', [win, bid, ui.size]);
   },

   fullsize: function(bid) {
      var win = jsxc.gui.window.get(bid);
      var size = jsxc.options.viewport.getSize();

      size.width -= 10;
      size.height -= win.find('.jsxc_bar').outerHeight() + win.find('.jsxc_textinput').outerHeight();

      jsxc.gui.window.resize(win, {
         size: size
      });
   },

   /**
    * Returns the window element
    *
    * @param {String} bid
    * @returns {jquery} jQuery object of the window element
    */
   get: function(id) {
      return $("li.jsxc_windowItem[data-bid='" + jsxc.jidToBid(id) + "']");
   },

   /**
    * Open a window, related to the bid. If the window doesn't exist, it will be
    * created.
    *
    * @param {String} bid
    * @returns {jQuery} Window object
    */
   open: function(bid) {
      var win = jsxc.gui.window.init(bid);

      jsxc.gui.window.show(bid);
      jsxc.gui.window.highlight(bid);

      return win;
   },

   /**
    * Close chatwindow and clean up
    *
    * @param {String} bid bar jid
    */
   close: function(bid) {

      if (jsxc.gui.window.get(bid).length === 0) {
         jsxc.warn('Want to close a window, that is not open.');
         return;
      }

      jsxc.storage.removeUserElement('windowlist', bid);
      jsxc.storage.removeUserItem('window', bid);

      if (jsxc.storage.getUserItem('buddylist').indexOf(bid) < 0) {
         // delete data from unknown sender

         jsxc.storage.removeUserItem('buddy', bid);
         jsxc.storage.removeUserItem('chat', bid);
      }

      jsxc.gui.window._close(bid);
   },

   /**
    * Close chatwindow
    *
    * @param {String} bid
    */
   _close: function(bid) {
      jsxc.gui.window.get(bid).remove();
      jsxc.gui.updateWindowListSB();
   },

   /**
    * Toggle between minimize and maximize of the text area
    *
    * @param {String} bid bar jid
    */
   toggle: function(bid) {

      var win = jsxc.gui.window.get(bid);

      if (win.parents("#jsxc_windowList").length === 0) {
         return;
      }

      if (win.hasClass('jsxc_min')) {
         jsxc.gui.window.show(bid);
      } else {
         jsxc.gui.window.hide(bid);
      }

      jsxc.gui.updateWindowListSB();
   },

   /**
    * Maximize text area and save
    *
    * @param {String} bid
    */
   show: function(bid) {

      jsxc.storage.updateUserItem('window', bid, 'minimize', false);

      return jsxc.gui.window._show(bid);
   },

   /**
    * Maximize text area
    *
    * @param {String} bid
    * @returns {undefined}
    */
   _show: function(bid) {
      var win = jsxc.gui.window.get(bid);
      var duration = 0;

      if (jsxc.isExtraSmallDevice()) {
         if (parseFloat($('#jsxc_roster').css('right')) >= 0) {
            duration = jsxc.gui.roster.toggle();
         }

         jsxc.gui.window.hide();
         jsxc.gui.window.fullsize(bid);
      }

      win.removeClass('jsxc_min').addClass('jsxc_normal');
      win.find('.jsxc_window').css('bottom', '0');

      setTimeout(function() {
         var padding = $("#jsxc_windowListSB").width();
         var innerWidth = $('#jsxc_windowList>ul').width();
         var outerWidth = $('#jsxc_windowList').width() - padding;

         if (innerWidth > outerWidth) {
            var offset = parseInt($('#jsxc_windowList>ul').css('right'));
            var width = win.outerWidth(true);

            var right = innerWidth - win.position().left - width + offset;
            var left = outerWidth - (innerWidth - win.position().left) - offset;

            if (left < 0) {
               jsxc.gui.scrollWindowListBy(left * -1);
            }

            if (right < 0) {
               jsxc.gui.scrollWindowListBy(right);
            }
         }
      }, duration);

      // If the area is hidden, the scrolldown function doesn't work. So we
      // call it here.
      jsxc.gui.window.scrollDown(bid);

      if (jsxc.restoreCompleted) {
         win.find('.jsxc_textinput').focus();
      }

      win.trigger('show.window.jsxc');
   },

   /**
    * Minimize text area and save
    *
    * @param {String} [bid]
    */
   hide: function(bid) {
      var hide = function(bid) {
         jsxc.storage.updateUserItem('window', bid, 'minimize', true);

         jsxc.gui.window._hide(bid);
      };

      if (bid) {
         hide(bid);
      } else {
         $('#jsxc_windowList > ul > li').each(function() {
            var el = $(this);

            if (!el.hasClass('jsxc_min')) {
               hide(el.attr('data-bid'));
            }
         });
      }
   },

   /**
    * Minimize text area
    *
    * @param {String} bid
    */
   _hide: function(bid) {
      var win = jsxc.gui.window.get(bid);

      win.removeClass('jsxc_normal').addClass('jsxc_min');
      win.find('.jsxc_window').css('bottom', -1 * win.find('.jsxc_fade').height());

      win.trigger('hidden.window.jsxc');
   },

   /**
    * Highlight window
    *
    * @param {type} bid
    */
   highlight: function(bid) {
      var el = jsxc.gui.window.get(bid).find(' .jsxc_bar');

      if (!el.is(':animated')) {
         el.effect('highlight', {
            color: 'orange'
         }, 2000);
      }
   },

   /**
    * Scroll chat area to the bottom
    *
    * @param {String} bid bar jid
    */
   scrollDown: function(bid) {
      var chat = jsxc.gui.window.get(bid).find('.jsxc_textarea');

      // check if chat exist
      if (chat.length === 0) {
         return;
      }

      chat.slimScroll({
         scrollTo: (chat.get(0).scrollHeight + 'px')
      });
   },

   /**
    * Write Message to chat area and save. Check border cases and remove html.
    *
    * @function postMessage
    * @memberOf jsxc.gui.window
    * @param {jsxc.Message} message object to be send
    * @return {jsxc.Message} maybe modified message object
    */
   /**
    * Create message object from given properties, write Message to chat area
    * and save. Check border cases and remove html.
    *
    * @function postMessage
    * @memberOf jsxc.gui.window
    * @param {object} args New message properties
    * @param {string} args.bid
    * @param {direction} args.direction
    * @param {string} args.msg
    * @param {boolean} args.encrypted
    * @param {boolean} args.forwarded
    * @param {boolean} args.sender
    * @param {integer} args.stamp
    * @param {object} args.attachment Attached data
    * @param {string} args.attachment.name File name
    * @param {string} args.attachment.size File size
    * @param {string} args.attachment.type File type
    * @param {string} args.attachment.data File data
    * @return {jsxc.Message} maybe modified message object
    */
   postMessage: function(message) {

      if (typeof message === 'object' && !(message instanceof jsxc.Message)) {
         message = new jsxc.Message(message);
      }

      var data = jsxc.storage.getUserItem('buddy', message.bid);

      if (!message.htmlMsg && message.msg) {
         message.htmlMsg = message.msg;
      }

      // remove html tags and reencode html tags
      message.msg = jsxc.removeHTML(message.msg);
      message.msg = jsxc.escapeHTML(message.msg);

      // exceptions:

      if (message.direction === jsxc.Message.OUT && data.msgstate === OTR.CONST.MSGSTATE_FINISHED && message.forwarded !== true) {
         message.direction = jsxc.Message.SYS;
         message.msg = $.t('your_message_wasnt_send_please_end_your_private_conversation');
      }

      if (message.direction === jsxc.Message.OUT && data.msgstate === OTR.CONST.MSGSTATE_FINISHED) {
         message.direction = 'sys';
         message.msg = $.t('unencrypted_message_received') + ' ' + message.msg;
      }

      message.encrypted = (typeof message.encrypted === 'boolean') ? message.encrypted : data.msgstate === OTR.CONST.MSGSTATE_ENCRYPTED;

      try {
         message.save();
      } catch (err) {
         jsxc.warn('Unable to save message.', err);

         message = new jsxc.Message({
            msg: 'Unable to save that message. Please clear some chat histories.',
            direction: jsxc.Message.SYS
         });
      }

      if (message.direction === 'in' && !jsxc.gui.window.get(message.bid).find('.jsxc_textinput').is(":focus")) {
         jsxc.gui.unreadMsg(message.bid);

         $(document).trigger('postmessagein.jsxc', [message.bid, message.htmlMsg]);
      }

      if (message.direction === jsxc.Message.OUT && jsxc.master && message.forwarded !== true && message.htmlMsg) {
         jsxc.xmpp.sendMessage(message);
      }

      jsxc.gui.window._postMessage(message);

      if (message.direction === 'out' && message.msg === '?' && jsxc.options.get('theAnswerToAnything') !== false) {
         if (typeof jsxc.options.get('theAnswerToAnything') === 'undefined' || (Math.random() * 100 % 42) < 1) {
            jsxc.options.set('theAnswerToAnything', true);

            jsxc.gui.window.postMessage(new jsxc.Message({
               bid: message.bid,
               direction: jsxc.Message.SYS,
               msg: '42'
            }));
         }
      }

      return message;
   },

   /**
    * Write Message to chat area
    *
    * @param {String} bid bar jid
    * @param {Object} post Post object with direction, msg, uid, received
    * @param {Bool} restore If true no highlights are used
    */
   _postMessage: function(message, restore) {
      var bid = message.bid;
      var win = jsxc.gui.window.get(bid);
      var msg = message.msg;
      var direction = message.direction;
      var uid = message._uid;

      if (win.find('.jsxc_textinput').is(':not(:focus)') && direction === jsxc.Message.IN && !restore) {
         jsxc.gui.window.highlight(bid);
      }

      msg = msg.replace(jsxc.CONST.REGEX.URL, function(url) {

         var href = (url.match(/^https?:\/\//i)) ? url : 'http://' + url;

         // @TODO use jquery element builder
         return '<a href="' + href + '" target="_blank">' + url + '</a>';
      });

      msg = msg.replace(new RegExp('(xmpp:)?(' + jsxc.CONST.REGEX.JID.source + ')(\\?[^\\s]+\\b)?', 'i'), function(match, protocol, jid, action) {
         if (protocol === 'xmpp:') {
            if (typeof action === 'string') {
               jid += action;
            }

            // @TODO use jquery element builder
            return '<a href="xmpp:' + jid + '">xmpp:' + jid + '</a>';
         }

         // @TODO use jquery element builder
         return '<a href="mailto:' + jid + '" target="_blank">mailto:' + jid + '</a>';
      });

      // replace emoticons from XEP-0038 and pidgin with shortnames
      $.each(jsxc.gui.emotions, function(i, val) {
         msg = msg.replace(val[2], ':' + val[1] + ':');
      });

      // translate shortnames to images
      msg = jsxc.gui.shortnameToImage(msg);

      // replace line breaks
      msg = msg.replace(/(\r\n|\r|\n)/g, '<br />');

      // replace /me command (XEP-0245)
      var bidData = jsxc.storage.getUserItem('buddy', bid) || {};
      if (direction === 'in') {
         msg = msg.replace(/^\/me /, '<i title="/me">' + jsxc.removeHTML(bidData.name || bid) + '</i> ');
      }

      // hide unprocessed otr messages
      if (msg.match(/^\?OTR([:,|?]|[?v0-9x]+)/)) {
         msg = '<i title="' + msg + '">' + $.t('Unreadable_OTR_message') + '</i>';
      }

      var msgDiv = $("<div>"),
         msgTsDiv = $("<div>");
      msgDiv.addClass('jsxc_chatmessage jsxc_' + direction);
      msgDiv.attr('id', uid.replace(/:/g, '-'));
      msgDiv.html('<div>' + msg + '</div>');
      msgTsDiv.addClass('jsxc_timestamp');
      msgTsDiv.text(jsxc.getFormattedTime(message.stamp));

      if (message.isReceived() || false) {
         msgDiv.addClass('jsxc_received');
      } else {
         msgDiv.removeClass('jsxc_received');
      }

      if (message.forwarded) {
         msgDiv.addClass('jsxc_forwarded');
      } else {
         msgDiv.removeClass('jsxc_forwarded');
      }

      if (message.encrypted) {
         msgDiv.addClass('jsxc_encrypted');
      } else {
         msgDiv.removeClass('jsxc_encrypted');
      }

      if (message.error) {
         msgDiv.addClass('jsxc_error');
      } else {
         msgDiv.removeClass('jsxc_error');
      }

      msgDiv.attr('title', message.error);
      msgDiv.attr('data-error-msg', message.error);

      if (message.attachment && message.attachment.name) {
         var attachment = $('<div>');
         attachment.addClass('jsxc_attachment');
         attachment.addClass('jsxc_' + message.attachment.type.replace(/\//, '-'));
         attachment.addClass('jsxc_' + message.attachment.type.replace(/^([^/]+)\/.*/, '$1'));

         if (message.attachment.persistent === false) {
            attachment.addClass('jsxc_notPersistent');
         }

         if (message.attachment.data) {
            attachment.addClass('jsxc_data');
         }

         if (message.attachment.type.match(/^image\//) && message.attachment.thumbnail) {
            $('<img alt="preview">').attr('src', message.attachment.thumbnail).attr('title', message.attachment.name).appendTo(attachment);
         } else {
            attachment.text(message.attachment.name);
         }

         if (message.attachment.data) {
            attachment = $('<a>').append(attachment);
            attachment.attr('href', message.attachment.data);
            attachment.attr('download', message.attachment.name);

            if (message.attachment.data === message.msg) {
               msgDiv.find('div').first().empty();
            }
         }

         msgDiv.find('div').first().append(attachment);
      }

      if (direction === 'sys') {
         jsxc.gui.window.get(bid).find('.jsxc_textarea').append('<div class="jsxc_clear"/>');
      } else if (typeof message.stamp !== 'undefined') {
         msgDiv.append(msgTsDiv);
      }

      if (direction !== 'sys') {
         jsxc.gui.window.setLastMsg(bid, msg);
      }

      var currentMessageElement = jsxc.Message.getDOM(uid);
      if (currentMessageElement.length > 0) {
         if (currentMessageElement.attr('data-queryId')) {
            msgDiv.attr('data-queryId', currentMessageElement.attr('data-queryId'));
         }
         currentMessageElement.replaceWith(msgDiv);
      } else {
         win.find('.jsxc_textarea').append(msgDiv);
      }

      if (typeof message.sender === 'object' && message.sender !== null) {
         var title = '';
         var avatarDiv = $('<div>');
         avatarDiv.addClass('jsxc_avatar').prependTo(msgDiv);

         if (typeof message.sender.jid === 'string') {
            msgDiv.attr('data-bid', jsxc.jidToBid(message.sender.jid));

            var data = jsxc.storage.getUserItem('buddy', jsxc.jidToBid(message.sender.jid)) || {};
            jsxc.gui.avatar.update(msgDiv, jsxc.jidToBid(message.sender.jid), data.avatar);

            title = jsxc.jidToBid(message.sender.jid);
         }

         if (typeof message.sender.name === 'string') {
            msgDiv.attr('data-name', message.sender.name);

            if (typeof message.sender.jid !== 'string') {
               jsxc.gui.avatarPlaceholder(avatarDiv, message.sender.name);
            }

            if (title !== '') {
               title = '\n' + title;
            }

            title = message.sender.name + title;

            msgTsDiv.text(msgTsDiv.text() + ' ' + message.sender.name);
         }

         avatarDiv.attr('title', jsxc.escapeHTML(title));

         if (msgDiv.prev().length > 0 && msgDiv.prev().find('.jsxc_avatar').attr('title') === avatarDiv.attr('title')) {
            avatarDiv.css('visibility', 'hidden');
         }
      }

      jsxc.gui.detectUriScheme(win);
      jsxc.gui.detectEmail(win);

      if (!message.forwarded) {
         jsxc.gui.window.scrollDown(bid);
      }
   },

   /**
    * Set text into input area
    *
    * @param {type} bid
    * @param {type} text
    * @returns {undefined}
    */
   setText: function(bid, text) {
      jsxc.gui.window.get(bid).find('.jsxc_textinput').val(text);
   },

   setLastMsg: function(bid, msg) {
      var lastMsgTextElement = $('[data-bid="' + bid + '"]').find('.jsxc_lastmsg .jsxc_text');

      lastMsgTextElement.html(msg);
      lastMsgTextElement.find('a').each(function() {
         $(this).replaceWith('<span>' + $(this).text() + '</span>');
      });
   },

   /**
    * Load old log into chat area
    *
    * @param {type} bid
    * @returns {undefined}
    */
   restoreChat: function(bid) {
      var chat = jsxc.storage.getUserItem('chat', bid);

      // convert legacy storage structure introduced in v3.0.0
      if (chat) {
         while (chat !== null && chat.length > 0) {
            var c = chat.pop();

            c.bid = bid;
            c._uid = c.uid;
            delete c.uid;

            var message = new jsxc.Message(c);
            message.save();

            jsxc.gui.window._postMessage(message, true);
         }

         jsxc.storage.removeUserItem('chat', bid);
      }

      var history = jsxc.storage.getUserItem('history', bid);

      while (history !== null && history.length > 0) {
         var uid = history.pop();

         jsxc.gui.window._postMessage(new jsxc.Message(uid), true);
      }
   },

   /**
    * Clear chat history
    *
    * @param {type} bid
    * @returns {undefined}
    */
   clear: function(bid) {
      // deprecated
      jsxc.storage.removeUserItem('chat', bid);

      var history = jsxc.storage.getUserItem('history', bid) || [];

      history.map(function(id) {
         jsxc.storage.removeUserItem('msg', id);
      });

      jsxc.storage.setUserItem('history', bid, []);

      var buddyData = jsxc.storage.getUserItem('buddy', bid) || {};
      delete buddyData.lastArchiveUid;
      delete buddyData.archiveExhausted;
      jsxc.storage.setUserItem('buddy', bid, buddyData);

      var win = jsxc.gui.window.get(bid);

      if (win.length > 0) {
         win.find('.jsxc_textarea').empty();
         win.find('.jsxc_textarea').scroll();
      }
   },

   /**
    * Mark message as received.
    *
    * @param  {string} bid
    * @param  {string} uid message id
    * @deprecated since v3.0.0. Use {@link jsxc.Message.received}.
    */
   receivedMessage: function(bid, uid) {
      jsxc.warn('Using deprecated receivedMessage.');

      var message = new jsxc.Message(uid);

      message.received();
   },

   updateProgress: function(message, sent, size) {
      var div = message.getDOM();
      var span = div.find('.jsxc_timestamp span');

      if (span.length === 0) {
         div.find('.jsxc_timestamp').append('<span>');
         span = div.find('.jsxc_timestamp span');
      }

      span.text(' ' + Math.round(sent / size * 100) + '%');

      if (sent === size) {
         span.remove();
      }
   },

   showOverlay: function(bid, content, allowClose) {
      var win = jsxc.gui.window.get(bid);

      win.find('.jsxc_overlay .jsxc_body').empty().append(content);
      win.find('.jsxc_overlay .jsxc_close').off('click').click(function() {
         jsxc.gui.window.hideOverlay(bid);
      });

      if (allowClose !== true) {
         win.find('.jsxc_overlay .jsxc_close').hide();
      } else {
         win.find('.jsxc_overlay .jsxc_close').show();
      }

      win.addClass('jsxc_showOverlay');
   },

   hideOverlay: function(bid) {
      var win = jsxc.gui.window.get(bid);

      win.removeClass('jsxc_showOverlay');
   },

   selectResource: function(bid, text, cb, res) {
      res = res || jsxc.storage.getUserItem('res', bid) || [];
      cb = cb || function() {};

      if (res.length > 0) {
         var content = $('<div>');
         var list = $('<ul>'),
            i, li;

         for (i = 0; i < res.length; i++) {
            li = $('<li>');

            li.append($('<a>').text(res[i]));
            li.appendTo(list);
         }

         list.find('a').click(function(ev) {
            ev.preventDefault();

            jsxc.gui.window.hideOverlay(bid);

            cb({
               status: 'selected',
               result: $(this).text()
            });
         });

         if (text) {
            $('<p>').text(text).appendTo(content);
         }

         list.appendTo(content);

         jsxc.gui.window.showOverlay(bid, content);
      } else {
         cb({
            status: 'unavailable'
         });
      }
   },

   smpRequest: function(bid, question) {
      var content = $('<div>');

      var p = $('<p>');
      p.text($.t('smpRequestReceived'));
      p.appendTo(content);

      var abort = $('<button>');
      abort.text($.t('Abort'));
      abort.click(function() {
         jsxc.gui.window.hideOverlay(bid);
         jsxc.storage.removeUserItem('smp', bid);

         if (jsxc.master && jsxc.otr.objects[bid]) {
            jsxc.otr.objects[bid].sm.abort();
         }
      });
      abort.appendTo(content);

      var verify = $('<button>');
      verify.text($.t('Verify'));
      verify.addClass('jsxc_btn jsxc_btn-primary');
      verify.click(function() {
         jsxc.gui.window.hideOverlay(bid);

         jsxc.otr.onSmpQuestion(bid, question);
      });
      verify.appendTo(content);

      jsxc.gui.window.showOverlay(bid, content);
   },

   sendFile: function(jid) {
      jsxc.fileTransfer.startGuiAction(jid);
   }
};

jsxc.gui.template = {};

/**
 * Return requested template and replace all placeholder
 *
 * @memberOf jsxc.gui.template;
 * @param {type} name template name
 * @param {type} bid
 * @param {type} msg
 * @returns {jQuery} HTML Template
 */
jsxc.gui.template.get = function(name, bid, msg) {

   // common placeholder
   var ph = {
      my_priv_fingerprint: jsxc.storage.getUserItem('priv_fingerprint') ? jsxc.storage.getUserItem('priv_fingerprint').replace(/(.{8})/g, '$1 ') : $.t('not_available'),
      my_jid: jsxc.storage.getItem('jid') || '',
      my_node: Strophe.getNodeFromJid(jsxc.storage.getItem('jid') || '') || '',
      root: jsxc.options.root,
      app_name: jsxc.options.app_name,
      version: jsxc.version
   };

   // placeholder depending on bid
   if (bid) {
      var data = jsxc.storage.getUserItem('buddy', bid);

      $.extend(ph, {
         bid_priv_fingerprint: (data && data.fingerprint) ? data.fingerprint.replace(/(.{8})/g, '$1 ') : $.t('not_available'),
         bid_jid: bid,
         bid_name: (data && data.name) ? jsxc.escapeHTML(data.name) : bid
      });
   }

   // placeholder depending on msg
   if (msg) {
      $.extend(ph, {
         msg: msg
      });
   }

   var ret = jsxc.gui.template[name];

   if (typeof(ret) === 'string') {
      // prevent 404
      ret = ret.replace(/\{\{root\}\}/g, ph.root);

      // encapsulate template to find all desired elements in the next step
      ret = $('<div>' + ret + '</div>');

      ret.find('[data-var]').each(function() {
         var key = $(this).attr('data-var');
         var val = (typeof ph[key] === 'string') ? ph[key] : '(Unknown placeholder: ' + key + ')';

         if ($(this).prop('tagName').toUpperCase() === 'INPUT') {
            $(this).val(val);
         } else {
            $(this).text(val);
         }
      });

      // remove encapsulation
      ret = ret.find('>*');

      ret.localize(ph);

      return ret;
   }

   jsxc.debug('Template not available: ' + name);
   return name;
};