text: /* eslint indent: "off" */
  
  var buddy_list = (function () {
      var self = {};
  
      self.container_sel = '#user_presences';
      self.scroll_container_sel = '#buddy_list_wrapper';
      self.item_sel = 'li.user_sidebar_entry';
      self.padding_sel = '#buddy_list_wrapper_padding';
  
      self.items_to_html = function (opts) {
          var user_info = opts.items;
          var html = templates.render('user_presence_rows', {users: user_info});
          return html;
      };
  
      self.item_to_html = function (opts) {
          var html = templates.render('user_presence_row', opts.item);
          return html;
      };
  
      self.get_li_from_key = function (opts) {
          var user_id = opts.key;
          var sel = self.item_sel + "[data-user-id='" + user_id + "']";
          return self.container.find(sel);
      };
  
      self.get_key_from_li = function (opts) {
          var user_id = opts.li.expectOne().attr('data-user-id');
          return user_id;
      };
  
      self.get_data_from_keys = function (opts) {
          var keys = opts.keys;
          var data = buddy_data.get_items_for_users(keys);
          return data;
      };
  
      self.compare_function = buddy_data.compare_function;
  
      self.height_to_fill = function () {
          // Because the buddy list gets sized dynamically, we err on the side
          // of using the height of the entire viewport for deciding
          // how much content to render.  Even on tall monitors this should
          // still be a significant optimization for orgs with thousands of
          // users.
          var height = message_viewport.height();
          return height;
      };
  
      // Try to keep code below this line generic, so that we can
      // extract a widget.
  
      self.keys = [];
  
      self.populate = function (opts) {
          self.render_count = 0;
          self.container.html('');
  
          // We rely on our caller to give us items
          // in already-sorted order.
          self.keys = _.map(opts.keys, function (k) {
              return k.toString();
          });
  
          self.fill_screen_with_content();
      };
  
      self.render_more = function (opts) {
          var chunk_size = opts.chunk_size;
  
          var begin = self.render_count;
          var end = begin + chunk_size;
  
          var more_keys = self.keys.slice(begin, end);
  
          if (more_keys.length === 0) {
              return;
          }
  
          var items = self.get_data_from_keys({
              keys: more_keys,
          });
  
          var html = self.items_to_html({
              items: items,
          });
          self.container = $(self.container_sel);
          self.container.append(html);
  
          // Invariant: more_keys.length >= items.length.
          // (Usually they're the same, but occasionally keys
          // won't return valid items.  Even though we don't
          // actually render these keys, we still "count" them
          // as rendered.
  
          self.render_count += more_keys.length;
          self.update_padding();
      };
  
      self.get_items = function () {
          var obj = self.container.find(self.item_sel);
          return obj.map(function (i, elem) {
              return $(elem);
          });
      };
  
      self.first_key = function () {
          return self.keys[0];
      };
  
      self.prev_key = function (key) {
          var i = self.keys.indexOf(key.toString());
  
          if (i <= 0) {
              return;
          }
  
          return self.keys[i - 1];
      };
  
      self.next_key = function (key) {
          var i = self.keys.indexOf(key.toString());
  
          if (i < 0) {
              return;
          }
  
          return self.keys[i + 1];
      };
  
      self.maybe_remove_key = function (opts) {
          var pos = self.keys.indexOf(opts.key);
  
          if (pos < 0) {
              return;
          }
  
          self.keys.splice(pos, 1);
  
          if (pos < self.render_count) {
              self.render_count -= 1;
              var li = self.find_li({key: opts.key});
              li.remove();
              self.update_padding();
          }
      };
  
      self.find_position = function (opts) {
          var key = opts.key;
          var i;
  
          for (i = 0; i < self.keys.length; i += 1) {
              var list_key = self.keys[i];
  
              if (self.compare_function(key, list_key) < 0) {
                  return i;
              }
          }
  
          return self.keys.length;
      };
  
      self.force_render = function (opts) {
          var pos = opts.pos;
  
          // Try to render a bit optimistically here.
          var cushion_size = 3;
          var chunk_size = pos + cushion_size - self.render_count;
  
          if (chunk_size <= 0) {
              blueslip.error('cannot show key at this position: ' + pos);
          }
  
          self.render_more({
              chunk_size: chunk_size,
          });
      };
  
      self.find_li = function (opts) {
          var key = opts.key.toString();
  
          // Try direct DOM lookup first for speed.
          var li = self.get_li_from_key({
              key: key,
          });
  
          if (li.length === 1) {
              return li;
          }
  
          if (!opts.force_render) {
              // Most callers don't force us to render a list
              // item that wouldn't be on-screen anyway.
              return li;
          }
  
          var pos = self.keys.indexOf(key);
  
          if (pos < 0) {
              // TODO: See list_cursor.get_row() for why this is
              //       a bit janky now.
              return [];
          }
  
          self.force_render({
              pos: pos,
          });
  
          li = self.get_li_from_key({
              key: key,
          });
  
          return li;
      };
  
      self.insert_new_html = function (opts) {
          var other_key = opts.other_key;
          var html = opts.html;
          var pos = opts.pos;
  
          if (other_key === undefined) {
              if (pos === self.render_count) {
                  self.render_count += 1;
                  self.container.append(html);
                  self.update_padding();
              }
              return;
          }
  
          if (pos < self.render_count) {
              self.render_count += 1;
              var li = self.find_li({key: other_key});
              li.before(html);
              self.update_padding();
          }
      };
  
      self.insert_or_move = function (opts) {
          var key = opts.key.toString();
          var item = opts.item;
  
          self.maybe_remove_key({key: key});
  
          var pos = self.find_position({
              key: key,
          });
  
          // Order is important here--get the other_key
          // before mutating our list.  An undefined value
          // corresponds to appending.
          var other_key = self.keys[pos];
  
          self.keys.splice(pos, 0, key);
  
          var html = self.item_to_html({item: item});
          self.insert_new_html({
              pos: pos,
              html: html,
              other_key: other_key,
          });
      };
  
      self.fill_screen_with_content = function () {
          var height_to_fill = self.height_to_fill();
  
          var elem = $(self.scroll_container_sel).expectOne()[0];
  
          // Add a fudge factor.
          height_to_fill += 10;
  
          while (self.render_count < self.keys.length) {
              var padding_height = $(self.padding_sel).height();
              var bottom_offset = elem.scrollHeight - elem.scrollTop - padding_height;
  
              if (bottom_offset > height_to_fill) {
                  break;
              }
  
              var chunk_size = 20;
  
              self.render_more({
                  chunk_size: chunk_size,
              });
          }
      };
  
      // This is a bit of a hack to make sure we at least have
      // an empty list to start, before we get the initial payload.
      self.container = $(self.container_sel);
  
      self.start_scroll_handler = function () {
          // We have our caller explicitly call this to make
          // sure everything's in place.
          var scroll_container = $(self.scroll_container_sel);
  
          scroll_container.scroll(function () {
              self.fill_screen_with_content();
          });
      };
  
      self.update_padding = function () {
          padded_widget.update_padding({
              shown_rows: self.render_count,
              total_rows: self.keys.length,
              content_sel: self.container_sel,
              padding_sel: self.padding_sel,
          });
      };
  
      return self;
  }());
  
  if (typeof module !== 'undefined') {
      module.exports = buddy_list;
  }
  
  window.buddy_list = buddy_list;
  