                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                /*
 * Copyright (c) 2006-2010 JS-Kit <support@js-kit.com>. All rights reserved.
 * You may copy and modify this script as long as the above copyright notice,
 * this condition and the following disclaimer is left intact.
 * This software is provided by the author "AS IS" and no warranties are
 * implied, including fitness for a particular purpose. In no event shall
 * the author be liable for any damages arising in any way out of the use
 * of this software, even if advised of the possibility of such damage.
 * $Id: echo-stream.js 27794 2010-10-08 13:45:27Z jskit $
 */

(function($) {
$.noConflict();

if (!window.Echo) window.Echo = {};
if (!Echo.Vars) Echo.Vars = {};

$.extend({
	"addCss": function(cssCode, id) {
		if (id) {
			id = 'echo-css-' + id;
			if ($('#' + id).length) return;
		}
		var $style = $('<style id="' + id + '" type="text/css">' + cssCode + '</style>');
		if (!Echo.Vars.$cssAnchor) {
			var container = document.getElementsByTagName("head")[0] || document.documentElement;
			$(container).prepend($style);
		} else {
			Echo.Vars.$cssAnchor.after($style);
		}
		Echo.Vars.$cssAnchor = $style;
	},
	"escapeURI": function(uri) {
		if (!uri) return;
		return (uri || "").replace(/&/g, '&amp;');
	},
	"foldl": function(acc, object, callback) {
		$.each(object, function(key, item) {
			result = callback(item, acc, key);
			if (result !== undefined) acc = result;
		});
		return acc;
	},
	"getDomain": function(uri) {
		return uri.replace(/^https?:\/\/(.*?)(\/.*|$)/ig, '$1');
	},
	"getNestedValue": function(key, data, defaults) {
		var found = true;
		var value = $.foldl(data, key.split(/\./), function(v, acc) {
			if (acc[v] === undefined) {
				found = false;
			} else {
				return acc[v];
			}
		});
		return found ? value : defaults;
	},
	"object2JSON": function(obj) {
		var encodeJSONLiteral = function(string) {
			var replacements = {
				'\b': '\\b',
				'\t': '\\t',
				'\n': '\\n',
				'\f': '\\f',
				'\r': '\\r',
				'"' : '\\"',
				'\\': '\\\\'};
			return string.replace(/[\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff\\]/g,
				function (a) {
					return (replacements.hasOwnProperty(a))
						? replacements[a]
						: '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
				}
			);
		}
		var out;
		switch (typeof(obj)) {
			case "number"  : out = isFinite(obj) ? obj : 'null'; break;
			case "string"  : out = '"' + encodeJSONLiteral(obj) + '"'; break;
			case "boolean" : out = '"' + obj.toString() + '"'; break;
			default :
				if (obj instanceof Array) {
					var container = $.map(obj, function(element) { return $.object2JSON(element); });
					out = '[' + container.join(",") + ']';
				} else if (obj instanceof Object) {
					var source = obj.exportProperties || obj;
					var container = $.foldl([], source, function(value, acc, property) {
						if (source instanceof Array) {
							property = value;
							value = obj[property];
						}
						acc.push('"' + property + '":' + $.object2JSON(value));
					});
					out = '{' + container.join(",") + '}';
				} else {
					out = 'null';
				}
		}
		return out;
	},
	"htmlTextTruncate": function(text, limit, postfix) {
		if (!limit || text.length < limit) return text;
		var tags = [], count = 0, finalPos = 0;
		var list = "br hr input img area param base link meta option".split(" ");
		var standalone = $.foldl({}, list, function(value, acc, key) {
			acc[value] = true;
		});
		for (var i = 0; i < text.length; i++) {
			var char = text.charAt(i);
			if (char == "<") {
				var tail = text.indexOf(">", i);
				if (tail < 0) return text;
				var source = text.substring(i + 1, tail);
				var tag = {"name": "", "closing": false};
				if (source[0] == "/") {
					tag.closing = true;
					source = source.substring(1);
				}
				tag.name = source.match(/(\w)+/)[0];
				if (tag.closing) {
					var current = tags.pop();
					if (!current || current.name != tag.name) return text;
				} else if (!standalone[tag.name]) {
					tags.push(tag);
				}
				i = tail;
			} else if (char == "&" && text.substring(i).match(/^(\S)+;/)) {
				i = text.indexOf(";", i);
			} else {
				if (count == limit) {
					finalPos = i;
					break;
				}
				count++;
			}
		}
		if (finalPos) {
			text = text.substring(0, finalPos) + (postfix || "");
			for (var i = tags.length - 1; i >= 0; i--) {
				text += "</" + tags[i].name + ">";
			}
		}
		return text;
	},
	"mapClass2Object": function(e, ctl) {
		var self = this;
		ctl = ctl || {};
		if (e.className) {
			var arr = String(e.className).split(/[ ]+/);
			$.each(arr, function(i, el) { ctl[el] = e });
		}
		try {
			$.each(e.childNodes, function(i, child) {
				$.mapClass2Object(child, ctl);
			});
		} catch(e) {}
		return ctl;
	},
	"stripTags": function(text) {
		return $('<div>').html(text).text();
	},
	"parseUrl": function(url, entities) {
		var parts = url.match("^((([^:/\\?#]+):)?//)?([^/\\?#]*)?([^\\?#]*)(\\?([^#]*))?(#(.*))?");
		return parts ? {
			"scheme": parts[3],
			"domain": parts[4],
			"path": parts[5],
			"query": parts[7],
			"fragment": parts[9]
		} : undefined;
	},
	"toDOM": function(template, prefix, renderer) {
		var content = $(template).get(0);
		var elements = $.mapClass2Object(content);
		var dom = {
			"set": function(name, element) {
				elements[prefix + name] = element;
			},
			"get": function(name, ignorePrefix) {
				return elements[(ignorePrefix ? "" : prefix) + name];
			},
			"content" : content
		};
		var rendererFunction;
		if (typeof renderer == 'object') {
			rendererFunction = function(name, element, dom) {
				if (!renderer[name]) return;
				return renderer[name](element, dom);
			}
		} else {
			rendererFunction = renderer;
		}
		$.each(elements, function(id, element) {
			var pattern = id.match(prefix + "(.*)");
			var name = pattern ? pattern[1] : undefined;
			if (name && rendererFunction) {
				var node = rendererFunction(name, element, dom);
				if (node) $(element).append(node);
			}
		});
		return dom;
	}
});

if (!Echo.Broadcast) Echo.Broadcast = {};

Echo.Broadcast.initContext = function(topic, contextId) {
	contextId = contextId || 'empty';
	Echo.Vars.subscriptions = Echo.Vars.subscriptions || {};
	Echo.Vars.subscriptions[contextId] = Echo.Vars.subscriptions[contextId] || {};
	Echo.Vars.subscriptions[contextId][topic] = Echo.Vars.subscriptions[contextId][topic] || {};
	return contextId;
}

Echo.Broadcast.subscribe = function(topic, handler, contextId) {
	var handlerId = (new Date()).valueOf() + Math.random();
	contextId = Echo.Broadcast.initContext(topic, contextId);
	Echo.Vars.subscriptions[contextId][topic][handlerId] = handler;
	return handlerId;
};

Echo.Broadcast.unsubscribe = function(topic, handlerId, contextId) {
	contextId = Echo.Broadcast.initContext(topic, contextId);
	if (topic && handlerId) {
		delete Echo.Vars.subscriptions[contextId][topic][handlerId];
	} else if (topic) {
		delete Echo.Vars.subscriptions[contextId][topic];
	}
};

Echo.Broadcast.publish = function(topic, data, contextId) {
	contextId = Echo.Broadcast.initContext(topic, contextId);
	if (contextId == '*') {
		$.each(Echo.Vars.subscriptions, function(ctxId) {
			$.each(Echo.Vars.subscriptions[ctxId][topic] || [], function(hId, handler) {
				handler.apply(this, [topic, data]);
			});
		});
	} else {
		if (Echo.Vars.subscriptions[contextId][topic]) {
			$.each(Echo.Vars.subscriptions[contextId][topic], function(hId, handler) {
				handler.apply(this, [topic, data]);
			});
		}
		if (contextId != 'empty') Echo.Broadcast.publish(topic, data, 'empty');
	}
};

if (!Echo.Object) Echo.Object = function() {};

Echo.Object.prototype.init = function(data) {
	$.extend(this, data || {});
};

Echo.Object.prototype.template = '';

Echo.Object.prototype.cssPrefix = "echo-";

Echo.Object.prototype.substitute = function(template, data) {
	template = template.replace(/{Label:([^:}]+[^}]*)}/g, function($0, $1) {
		return Echo.Localization.getLabel($1);
	});
	template = template.replace(/{Data:(([a-z]+\.)*[a-z]+)}/ig, function($0, $1) {
		return $.getNestedValue($1, data, '');
	});
	return template;
};

Echo.Object.prototype.renderers = {};

Echo.Object.prototype.render = function(name, element, dom, extra) {
	var self = this;
	if (name) {
		if ($.isFunction(this.renderers[name])) {
			return this.renderers[name].call(this, element, dom, extra);
		}
	} else {
		var tmpl = $.isFunction(this.template) ? this.template() : this.template;
		this.dom = $.toDOM(this.substitute(tmpl, this.data), this.cssPrefix, function() {
			return self.render.apply(self, arguments);
		});
		return this.dom.content;
	}
};

Echo.Object.prototype.rerender = function(name) {
	if (name) {
		if (this.dom && this.dom.get(name)) {
			this.renderers[name].call(this, this.dom.get(name), this.dom);
		}
	} else {
		$(this.dom.content).replaceWith(this.render());
	}
};

Echo.Object.prototype.hyperlink = function(data, options) {
	options = options || {}
	if (options.openInNewWindow && !data.target) {
		data.target = '_blank';	
	}
	var link = {
		"Attributes" : $.foldl([], data, function(value, acc, key){
			if (key != 'caption'){
				if (('href' == key) && !options.skipEscaping) {
					value = $.escapeURI(value);
				}
				acc.push(key + '="' + value + '"');
			}
		}).join(" "),
		"Caption" : (data.caption || "")
	};
	return this.substitute('<a {Data:Attributes}>{Data:Caption}</a>', link);
};

Echo.Object.prototype.newContextId = function() {
	return (new Date()).valueOf() + Math.random();
};

Echo.Object.prototype.subscribe = function(topic, handler) {
	return Echo.Broadcast.subscribe(topic, handler, this.contextId);
};

Echo.Object.prototype.unsubscribe = function(topic, handlerId) {
	Echo.Broadcast.unsubscribe(topic, handlerId, this.contextId);
};

Echo.Object.prototype.publish = function(topic, data) {
	Echo.Broadcast.publish(topic, data, this.contextId);
};

Echo.Application = function() {
	var id = 'echo-css';
	if ($('#' + id).length) return;
	var container;
	var head = $('head');
	if (head.length) {
		container = head;
	} else {
		contaner = $(document.body);
	}
	container.prepend('<link id="' + id + '" rel="stylesheet" type="text/css" href="//js-kit.com/css/fancybox.css">');

	$.addCss(
		'.echo-application-message { padding: 15px 0px; text-align: center; -moz-border-radius: 0.5em; -webkit-border-radius: 0.5em; border: 1px solid #E4E4E4; }' +
		'.echo-application-message-icon { display: inline-block; height: 16px; padding-left: 16px; background: no-repeat left center; }' +
		'.echo-application-message .echo-application-message-icon { padding-left: 21px; height: auto; }' +
		'.echo-application-message-empty { background-image: url(//cdn.js-kit.com/images/information.png); }' +
		'.echo-application-message-loading { background-image: url(//cdn.js-kit.com/images/loading.gif); }' +
		'.echo-application-message-error { background-image: url(//cdn.js-kit.com/images/warning.gif); }'
	, 'application');
};

Echo.Application.prototype = new Echo.Object();

Echo.Application.prototype.initApplication = function() {
	if (this.config && !this.config.get("appkey")) {
		this.showMessage({
			"type": "error",
			"message": "incorrect_appkey: Incorrect or missing mandatory parameter appkey"
		});
		return false;
	}
	return true;
};

Echo.Application.prototype.messageTemplates = {
	'compact': '<span class="echo-application-message-icon echo-application-message-{Data:type}" title="{Data:message}"></span>',
	'default': '<div class="echo-application-message">' +
		'<span class="echo-application-message-icon echo-application-message-{Data:type} echo-primaryFont">{Data:message}</span>' +
	'</div>'
};

Echo.Application.prototype.showMessage = function(data, target) {
	target = $(target || this.target);
	target.empty().append(this.substitute(this.messageTemplates[data.layout || this.messageLayout || "default"], data));
};

Echo.User = Echo.User || {
	"data": {},
	"serverBaseURL": "http://api.js-kit.com",
	"initialization": "none"
};

Echo.User.init = function(callback) {
	var self = this;
	if (!window.Backplane || !Backplane.getChannelID()) {
		this.reset();
		callback();
		return;
	}
	if (this.initialization == "complete") {
		callback();
		return false;
	} else {
		var handlerId = Echo.Broadcast.subscribe("User.onInit", function() {
			Echo.Broadcast.unsubscribe("User.onInit", handlerId);
			callback();
		});
		if (this.initialization != "none") return false;
	}
	this.initialization = "waiting";
	Backplane.subscribe(function(message) {
		var data = message.payload && message.payload.session || {};
		switch (message.type) {
			case "session/ready":
				self.request(function() {
					Echo.Broadcast.publish("User.onInvalidate", {"data": data}, "*");
				});
			break;
			case "identity/logout":
				Backplane.resetCookieChannel();
				self.reset($.extend({}, data));
				Echo.Broadcast.publish("User.onInvalidate", {"data": data}, "*");
			break;
		}
	});
	this.request();
};

Echo.User.reset = function(data) {
	this.data = this.normalize(data);
	this.account = this.assemble();
};

Echo.User.logout = function(callback) {
	var self = this;
	$.get("http://js-kit.com/apps/logout", {
		"sessionID": Backplane.getChannelID()
	}, function(data) {
		Backplane.expectMessagesWithin(10);
	}, "jsonp");
};

Echo.User.request = function(callback) {
	var self = this;
	this.initialization = "waiting";
	$.get(this.serverBaseURL + "/v1/users/whoami", {
		"sessionID": Backplane.getChannelID()
	}, function(data) {
		if (data.result && data.result == "session_not_found") {
			data = {};
		}
		self.reset($.extend({}, data));
		self.initialization = "complete";
		Echo.Broadcast.publish("User.onInit", {"data": data});
		if (callback) callback();
	}, "jsonp");
};

Echo.User.normalize = function(data) {
	data = data || {};
	data.echo = data.echo || {};
	data.poco = data.poco || {"entry":{}};
	data.roles = $.foldl({}, data.echo.roles || [], function(role, acc) {
		acc[role] = true;
	});
	data.sessionID = window.Backplane && Backplane.getChannelID() || undefined;
	// merge poco & echo accounts data
	data.accounts = $.each(data.poco.entry.accounts || [], function(i, account) {
		if (data.echo.accounts && data.echo.accounts[i]) {
			$.map(["loggedIn", "identityUrl"], function(key) {
				account[key] = data.echo.accounts[i][key];
			});
		}
		return account;
	});
	return data;
};

Echo.User.getActiveAccounts = function() {
	return $.map(this.data.accounts, function(entry) {
		if (entry.loggedIn) return entry;
	});
};

Echo.User.assemble = function() {
	var accounts = this.getActiveAccounts();
	var account = accounts[0] || {};
	return {
		"id": account.identityUrl || this.data.poco.entry.id || account.userid,
		"name": this.data.poco.entry.displayName || account.username,
		"avatar": $.foldl(undefined, account.photos || [], function(img) {
			if (img.type == "avatar") return img.value;
		}),
		"roles": this.data.roles,
		"domain": account.domain,
		"logged": account.loggedIn,
		"sessionID": this.data.sessionID,
		"defaultAvatar": "//js-kit.com/avatar/gxpA99f0jKlohF_DgthroT.png"
	};
};

Echo.User.hasIdentity = function(id) {
	var hasIdentity = false;
	$.map(this.data.accounts, function(account) {
		if (account.identityUrl && account.identityUrl == id) {
			hasIdentity = true;
			return false; // break
		}
	});
	return hasIdentity;
};

Echo.User.hasRole = function(roles) {
	if (!this.account) return false;
	var self = this, hasRole = false;
	$.map(roles, function(role) {
		if (self.account.roles[role]) {
			hasRole = true;
			return false; // break
		}
	});
	return hasRole;
};

Echo.User.logged = function() {
	return this.account && this.account.logged;
};

Echo.User.get = function(key, defaults) {
	return (this.account.hasOwnProperty(key) && typeof(this.account[key]) != "undefined") ? this.account[key] : defaults;
};

Echo.Config = function(primary, defaults, normalizer) {
	var self = this;
	this.data = {};
	this.normalize = normalizer || function(key, value) { return value; };
	$.each(this.combine(primary, defaults || {}), function(key, value) {
		self.set(key, value);
	});
};

Echo.Config.prototype.get = function(key) {
	return $.getNestedValue(key, this.data);
};

Echo.Config.prototype.set = function(key, value) {
	this.data[key] = this.normalize(key, value);
};

Echo.Config.prototype.combine = function(primary, defaults) {
	var self = this;
	return $.foldl(defaults, primary, function(value, acc, key) {
		acc[key] = $.isPlainObject(value)
			? self.combine(value, defaults[key] || {})
			: value;
	});
};

Echo.Config.prototype.extend = function(extra) {
	var self = this;
	$.each(extra, function(key, value) {
		self.set(key, value);
	});
};

if (!Echo.UI) Echo.UI = {
	cornersCss: function(radius, scopeClass) {
		scopeClass = scopeClass || '';
		return scopeClass + '.ui-corner-tl { -moz-border-radius-topleft: ' + radius + '; -webkit-border-top-left-radius: ' + radius + '; border-top-left-radius: ' + radius + '; }' +
		scopeClass + '.ui-corner-tr { -moz-border-radius-topright: ' + radius + '; -webkit-border-top-right-radius: ' + radius + '; border-top-right-radius: ' + radius + '; }' +
		scopeClass + '.ui-corner-bl { -moz-border-radius-bottomleft: ' + radius + '; -webkit-border-bottom-left-radius: ' + radius + '; border-bottom-left-radius: ' + radius + '; }' +
		scopeClass + '.ui-corner-br { -moz-border-radius-bottomright: ' + radius + '; -webkit-border-bottom-right-radius: ' + radius + '; border-bottom-right-radius: ' + radius + '; }' +
		scopeClass + '.ui-corner-top { -moz-border-radius-topleft: ' + radius + '; -webkit-border-top-left-radius: ' + radius + '; border-top-left-radius: ' + radius + '; -moz-border-radius-topright: ' + radius + '; -webkit-border-top-right-radius: ' + radius + '; border-top-right-radius: ' + radius + '; }' +
		scopeClass + '.ui-corner-bottom { -moz-border-radius-bottomleft: ' + radius + '; -webkit-border-bottom-left-radius: ' + radius + '; border-bottom-left-radius: ' + radius + '; -moz-border-radius-bottomright: ' + radius + '; -webkit-border-bottom-right-radius: ' + radius + '; border-bottom-right-radius: ' + radius + '; }' +
		scopeClass + '.ui-corner-right {  -moz-border-radius-topright: ' + radius + '; -webkit-border-top-right-radius: ' + radius + '; border-top-right-radius: ' + radius + '; -moz-border-radius-bottomright: ' + radius + '; -webkit-border-bottom-right-radius: ' + radius + '; border-bottom-right-radius: ' + radius + '; }' +
		scopeClass + '.ui-corner-left { -moz-border-radius-topleft: ' + radius + '; -webkit-border-top-left-radius: ' + radius + '; border-top-left-radius: ' + radius + '; -moz-border-radius-bottomleft: ' + radius + '; -webkit-border-bottom-left-radius: ' + radius + '; border-bottom-left-radius: ' + radius + '; }' +
		scopeClass + '.ui-corner-all { -moz-border-radius: ' + radius + '; -webkit-border-radius: ' + radius + '; border-radius: ' + radius + '; }';
	}
};

(function() {
	$.addCss(
		'.echo-ui .ui-helper-hidden { display: none; }' +
		'.echo-ui .ui-helper-hidden-accessible { position: absolute; left: -99999999px; }' +
		'.echo-ui .ui-helper-reset { margin: 0; padding: 0; border: 0; outline: 0; line-height: 1.3; text-decoration: none; font-size: 100%; list-style: none; }' +
		'.echo-ui .ui-helper-clearfix:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; }' +
		'.echo-ui .ui-helper-clearfix { display: inline-block; }' +
		'/* required comment for clearfix to work in Opera \\*/' +
		'* html .echo-ui .ui-helper-clearfix { height:1%; }' +
		'.echo-ui .ui-helper-clearfix { display:block; }' +
		'/* end clearfix */' +
		'.echo-ui .ui-helper-zfix { width: 100%; height: 100%; top: 0; left: 0; position: absolute; opacity: 0; filter:Alpha(Opacity=0); }' +
		'.echo-ui .ui-resizable-handle { position: absolute;font-size: 0.1px;z-index: 99999; display: block;}' +
		'.echo-ui .ui-resizable-disabled .ui-resizable-handle, .ui-resizable-autohide .ui-resizable-handle { display: none; }' +
		'.echo-ui .ui-resizable-n { cursor: n-resize; height: 7px; width: 100%; top: -5px; left: 0; }' +
		'.echo-ui .ui-resizable-s { cursor: s-resize; height: 7px; width: 100%; bottom: -5px; left: 0; }' +
		'.echo-ui .ui-resizable-e { cursor: e-resize; width: 7px; right: -5px; top: 0; height: 100%; }' +
		'.echo-ui .ui-resizable-w { cursor: w-resize; width: 7px; left: -5px; top: 0; height: 100%; }' +
		'.echo-ui .ui-resizable-se { cursor: se-resize; width: 12px; height: 12px; right: 1px; bottom: 1px; }' +
		'.echo-ui .ui-resizable-sw { cursor: sw-resize; width: 9px; height: 9px; left: -5px; bottom: -5px; }' +
		'.echo-ui .ui-resizable-nw { cursor: nw-resize; width: 9px; height: 9px; left: -5px; top: -5px; }' +
		'.echo-ui .ui-resizable-ne { cursor: ne-resize; width: 9px; height: 9px; right: -5px; top: -5px;}' +
		'.echo-ui .ui-state-disabled { cursor: default !important; }' +
		'.echo-ui .ui-icon { display: block; text-indent: -99999px; overflow: hidden; background-repeat: no-repeat; width: 16px; height: 16px; }' +
		'.echo-ui .ui-widget-header { font-weight: bold; border: 0px; }' +
		'.echo-ui, .echo-ui .ui-widget :active { outline: none; }' +
		'.echo-ui .ui-state-default { border: 1px solid #d3d3d3; background: #e6e6e6; color: #555555; }' +
		'.echo-ui .ui-state-default a, .echo-ui .ui-state-default a:link, .echo-ui .ui-state-default a:visited { color: #555555; text-decoration: none; }' +
		'.echo-ui .ui-state-hover, .echo-ui .ui-state-focus { border: 1px solid #999999; background: #dfebf2; color: #212121; }' +
		'.echo-ui .ui-state-hover a, .echo-ui .ui-state-hover a:hover { color: #212121; text-decoration: none; }' +
		'.echo-ui .ui-state-active { border: 1px solid #aaaaaa; background: #dfebf2; color: #212121; }' +
		'.echo-ui .ui-state-active a, .echo-ui .ui-state-active a:link, .echo-ui .ui-state-active a:visited { color: #212121; text-decoration: none; }' +

		'.echo-primaryBackgroundColor {  }' +
		'.echo-secondaryBackgroundColor { background-color: #F4F4F4; }' +
		'.echo-trinaryBackgroundColor { background-color: #ECEFF5; }' +
		'.echo-primaryColor { color: #3A3A3A; }' +
		'.echo-secondaryColor { color: #C6C6C6; }' +
		'.echo-primaryFont { font-family: Arial, sans-serif; font-size: 12px; font-weight: normal; line-height: 16px; }' +
		'.echo-secondaryFont { font-family: Arial, sans-serif; font-size: 11px; }' +
		'.echo-linkColor { color: #476CB8; }' +
		'.echo-clickable { cursor: pointer; }' +
		'.echo-relative { position: relative; }' +
		'.echo-clear { clear: both; }'
	, 'ui-general');
})();

Echo.UI.Dialog = function(data) {
	data.config = data.config || {};
	this.init(data);
	this.config.dialogClass = 'echo-ui echo-dialog ' + (this.config.dialogClass || '');
	this.addCss();
	this.contentElement = $(this.render()).dialog(this.config).addClass('ui-corner-all');
	if (this.content) {
		if ($.isFunction(this.content)) {
			this.content(this.contentElement);
		} else {
			this.contentElement.append(this.content);
		}
	}
	this.widget = this.contentElement.dialog('widget');
	if (this.hasTabs) {
		// move tabs line to dialog header to prevent tabs scrolling
		$('.ui-dialog-titlebar', this.widget).after($('.echo-tabs-header', this.widget));
	}
};

Echo.UI.Dialog.prototype = new Echo.Object();

Echo.UI.Dialog.prototype.cssPrefix = "echo-dialog-";

Echo.UI.Dialog.prototype.template =
	'<div></div>';

Echo.UI.Dialog.prototype.open = function() {
	// hide contentElement for jquery to calculate dialog height correctly in IE
	this.contentElement.hide();
	this.contentElement.dialog('open');
	this.contentElement.show();
};

Echo.UI.Dialog.prototype.close = function() {
	this.contentElement.dialog('close');
};

Echo.UI.Dialog.prototype.addCss = function() {
	$.addCss(
		'.echo-dialog { position: absolute; padding: 0px 7px 20px 7px; width: 300px; border: 1px solid #aaaaaa; background: #dfebf2; -moz-border-radius: 7px; -webkit-border-radius: 7px; border-radius: 7px;' + (!$.browser.msie ? ' overflow: hidden;' : '') + ' }' +
		'.echo-dialog .ui-dialog-titlebar { background: #dfebf2; cursor: move; padding: 7px 0px 10px 5px; position: relative; color: #4a4a4a; font: 18px Helvetica,sans-serif; }' +
		'.echo-dialog .ui-dialog-titlebar .ui-state-default, .echo-dialog .ui-dialog-titlebar .ui-state-active, .echo-dialog .ui-dialog-titlebar .ui-state-hover, .echo-dialog .ui-dialog-titlebar .ui-state-focus { border: 0px; background: none; }' +
		'.echo-dialog .ui-dialog-title { float: left; margin: .1em 16px .2em 0; } ' +
		'.echo-dialog .ui-dialog-titlebar-close { position: absolute; right: 0px; top: 50%; width: 19px; margin: -10px 0 0 0; padding: 0px; height: 18px; }' +
		'.echo-dialog .ui-dialog-titlebar-close span { display: block; margin: 1px; }' +
		'.echo-dialog .ui-dialog-titlebar-close:hover, .ui-dialog .ui-dialog-titlebar-close:focus { padding: 0px; }' +
		'.echo-dialog .ui-dialog-content { border: 0; padding: 0px; margin: 0px; background: #ffffff; overflow: auto; zoom: 1; }' +
		'.echo-dialog .ui-resizable-se { width: 14px; height: 14px; right: 3px; bottom: 3px; }' +
		'.echo-dialog .ui-icon-closethick { background: no-repeat top right url(//cdn.js-kit.com/images/container/closeWindow.png); }' +
		'.echo-dialog .ui-icon-grip-diagonal-se { background: no-repeat bottom right url(//cdn.js-kit.com/images/container/resizeHandle.png); }' +
		Echo.UI.cornersCss('7px', '.echo-dialog ')
	, 'ui-dialog');
};

Echo.UI.Tabs = function(data) {
	var self = this;
	data.config = data.config || {};
	this.init(data);
	if (!this.tabs) return;
	var classPrefix = this.idPrefix;
	// add random part to get unique id
	this.idPrefix = this.idPrefix + Math.ceil(Math.random() * 999999999) + '-';
	this.addCss();
	var disabledTabs = $.foldl([], this.tabs, function(tab, acc, i) {
		tab.classPrefix = classPrefix;
		tab.idPrefix = self.idPrefix;
		if (tab.icon) {
			tab.label = '<span>' + tab.label + '</span>';
		}
		if (tab.disabled) {
			acc.push(i);
		}
	});
	this.target.append($(this.render()));
	this.tabIndexById = {};
	$.each(this.tabs, function(i, tab) {
		self.tabIndexById[tab.id] = i;
		if (tab.content) {
			var tgt = $('#' + tab.idPrefix + tab.id);
			if ($.isFunction(tab.content)) {
				tab.content(tgt);
			} else {
				tgt.append(tab.content);
			}
		}
	});
	// if tabs will be placed into another UI element (dialog, another tabs) better not to add another echo-ui class
	if (this.addUIClass !== false) {
		this.target.addClass('echo-ui');
	}
	$.extend(this.config, {
		"disabled": disabledTabs.concat(self.config.disabled || []),
		"select": function(event, ui) {
			self.content[ui.index ? 'addClass' : 'removeClass']('ui-corner-tl');
		}
	});
	this.headerElement = $('.echo-tabs-header', this.target).tabs(this.config);
	this.panelsElement = $('.echo-tabs-panels', this.target).tabs(this.config);
	$('.echo-tabs-header, .echo-tabs-header .ui-tabs-nav', this.target).removeClass('ui-corner-all');
	this.content = $(this.content || '.echo-tabs-panels', this.target);
	// top right corner of content panel should not be rounded while first tab is selected
	this.content.removeClass('ui-corner-all').addClass('ui-corner-tr ui-corner-bottom');
};

Echo.UI.Tabs.prototype = new Echo.Object();

Echo.UI.Tabs.prototype.cssPrefix = "echo-tabs-";

Echo.UI.Tabs.prototype.template = function() {
	var self = this;
	return '<div class="echo-tabs">' +
		'<div class="echo-tabs echo-tabs-header">' +
			'<ul>' +
				$.map(this.tabs, function(tab) {
					return self.substitute('<li><a class="echo-{Data:classPrefix}{Data:id}" href="#{Data:idPrefix}{Data:id}">{Data:label}</a></li>', tab);
				}).join("\n") +
			'</ul>' +
		'</div>' +
		'<div class="echo-tabs echo-tabs-panels"></div>' +
	'</div>';
};

Echo.UI.Tabs.prototype.renderers = {};

Echo.UI.Tabs.prototype.renderers.panels = function(element) {
	var self = this;
	$.each(this.tabs, function(i, tab) {
		var node = $.toDOM(self.substitute('<div id="{Data:idPrefix}{Data:id}" class="{Data:idPrefix}{Data:id}"></div>', tab));
		$(element).append(node.content);
	});
};

Echo.UI.Tabs.prototype.select = function(id) {
	this.headerElement.tabs('select', this.tabIndexById[id]);
}

Echo.UI.Tabs.prototype.addCss = function() {
	$.addCss(
		'.echo-ui .ui-tabs { position: relative; padding: 0px; zoom: 1; border: 0px; }' +
		'.echo-tabs .echo-tabs-panels { background: #ffffff; }' +
		'.echo-ui .ui-tabs .ui-tabs-nav { margin: 0; padding: 0px; }' +
		'.echo-ui .ui-tabs .ui-tabs-nav li { list-style: none; float: left; position: relative; top: 1px; margin: 0 .2em 1px 0; border-bottom: 0 !important; padding: 0; white-space: nowrap; }' +
		'.echo-ui .ui-tabs .ui-tabs-nav li a { float: left; padding: .3em .7em; text-decoration: none; font-size: 12px; font-family: Helvetica,sans-serif; }' +
		'.echo-ui .ui-tabs .ui-tabs-nav li.ui-tabs-selected { margin-bottom: 0; padding-bottom: 1px; }' +
		'.echo-ui .ui-tabs .ui-tabs-nav li.ui-tabs-selected a, .echo-ui .ui-tabs .ui-tabs-nav li.ui-state-disabled a, .echo-ui .ui-tabs .ui-tabs-nav li.ui-state-processing a { cursor: text; color: #4a4a4a; }' +
		'.echo-ui .ui-tabs .ui-tabs-nav li a, .echo-ui .ui-tabs.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-selected a { cursor: pointer; color: #393939; }' +
		'.echo-ui .ui-tabs .ui-tabs-panel { display: block; border-width: 0; padding: 1em 1.4em; background: none; }' +
		'.echo-ui .ui-tabs .ui-tabs-hide { display: none !important; }' +
		'.echo-ui .echo-tabs-header .ui-state-hover, .echo-ui .echo-tabs-header .ui-state-focus { border: 0px; background: none; color: #212121; }' +
		'.echo-ui .echo-tabs-header .ui-state-default { border: 0px; background: none; font-weight: normal; }' +
		'.echo-ui .echo-tabs-header .ui-state-active { border: 0px; background: #ffffff; font-weight: bold; }' +
		'.echo-ui .ui-tabs .ui-tabs-nav li a span { display: inline-block; padding-left: 22px; }' +
		($.browser.opera ? '.echo-ui .ui-tabs-nav { height: 25px; overflow: hidden; }' : '') +
		Echo.UI.cornersCss('7px', '.echo-tabs ')
	, 'ui-tabs');
};

if (!Echo.Localization) Echo.Localization = { labels: {} };

Echo.Localization.extend = function(labels) {
	$.each(labels, function(name, value) {
		Echo.Localization.labels[name] = value;
	});
};

Echo.Localization.getLabel = function(label, data) {
	var label = Echo.Localization.labels[label] || label;
	$.each(data || {}, function(key, value) {
		label = label.replace(new RegExp("{" + key + "}", "g"), value);
	});
	return label;
};

Echo.Localization.extend({
	"userList_and": "and",
	"userList_guest": "Guest",
	"userList_guests": "Guests",
	"userList_more": "more",
	"userList_you": "You"
});

Echo.UsersList = function(target, config) {
	if (!target) return;
	var self = this;
	this.target = target;
	this.config = new Echo.Config(config || {}, {
		"itemsPerPage": 2,
		"suffixText": "",
		"totalUsersCount": 0,
		"userLabel": {
			"avatar": true,
			"text": true
		},
		"visibleUsersCount": undefined
	});
	this.messageLayout = 'compact';
	this.users = this.config.get('data') || [];
	this.totalUsersCount = Math.max(this.users.length, this.config.get("totalUsersCount"));
	this.visibleUsersCount = this.config.get('visibleUsersCount') ||
						this.config.get('itemsPerPage');
	var currentUserIdx;
	$.each(this.users, function(i, user) {
		user.isGuest = false;
		if (!user.avatar) {
			user.avatar = Echo.User.get("defaultAvatar");
		}
		if (self.isCurrentUser(user)) {
			user.title = Echo.Localization.getLabel('userList_you');
			self.currentUser = user;
			currentUserIdx = i;
		} else if (self.isGuest(user)) {
			user.isGuest = true;
		}
	});
	if (currentUserIdx) {
		this.users.splice(currentUserIdx, 1);
		this.users.unshift(this.currentUser);
	}
	this.addCss();
	$(this.target).empty().append(this.render());
};

Echo.UsersList.prototype = new Echo.Application();

Echo.UsersList.prototype.cssPrefix = "echo-user-list-";

Echo.UsersList.prototype.template =
	'<span class="echo-user-list-container"></span>';

Echo.UsersList.prototype.renderers = {};

Echo.UsersList.prototype.renderers.container = function(element) {
	var self = this;
	element = $(element);
	var usersHtml = [];
	var guestsCount = 0;
	if (!this.users.length || !this.config.get('userLabel.avatar') && !this.config.get('userLabel.text')) return;
	$.each(this.users, function(i, user) {
		if (i >= self.visibleUsersCount) return false;
		if (user.isGuest) {
			guestsCount++;
		} else if (self.currentUser && user == self.currentUser) {
			usersHtml.unshift(self.substitute(self.userTemplate(), user));
		} else {
			usersHtml.push(self.substitute(self.userTemplate(), user));
		}
	});
	if (guestsCount) {
		usersHtml.push(this.substitute(
			this.userTemplate(),
			{
				"avatar": Echo.User.get("defaultAvatar"),
				"count": guestsCount,
				"title": ' ' + Echo.Localization.getLabel('userList_guest' + (guestsCount > 1 ? 's' : ''))
			}
		));
	}
	if (this.visibleUsersCount < this.totalUsersCount) {
		var template = '{Data:count} {Label:userList_more}';
		if (this.visibleUsersCount < this.users.length) {
			template = '<a href="" class="echo-user-list-more">' + template + '</a>';
		}
		usersHtml.push(this.substitute(template, {
			"count": this.totalUsersCount - this.visibleUsersCount
		}));
	}
	var last = usersHtml.pop();
	var delimiter = this.config.get('userLabel.text') ? ', ' : '';
	var html = (usersHtml.length ? usersHtml.join(delimiter) + ' ' + Echo.Localization.getLabel('userList_and') + ' ' : '' ) + last;
	$(element).html(html + this.config.get('suffixText'));
	$('.echo-user-list-more', element).click(function(e) {
		e.preventDefault();
		self.visibleUsersCount += self.config.get('itemsPerPage');
		if (self.visibleUsersCount > self.users.length) {
			self.visibleUsersCount = self.users.length;
		}
		self.rerender();
	});
};

Echo.UsersList.prototype.userTemplate = function() {
	var html = '<span>';
	if (this.config.get('userLabel.avatar')) {
		html += '<img src="{Data:avatar}" width="16" height="16">';
	}
	if (this.config.get('userLabel.text')) {
		html += '{Data:count}{Data:title}';
	} else {
		html = '{Data:count}' + html;
	}
	html += '</span>';
	return html;
};

Echo.UsersList.prototype.isGuest = function(user) {
	return !user.title;
};

Echo.UsersList.prototype.isCurrentUser = function(user) {
	return user.id == Echo.User.get('id');
};

Echo.UsersList.prototype.getVisibleUsersCount = function() {
	return this.visibleUsersCount;
};

Echo.UsersList.prototype.addCss = function() {
	$.addCss(
		'.echo-user-list-container { line-height: 20px; vertical-align: middle; }' +
		'.echo-user-list-container span { white-space: nowrap; }' +
		'.echo-user-list-container img { margin: 0px 3px 0px 0px; vertical-align: middle; }'
	, 'users-list');
};

Echo.Localization.extend({
	"actions": "Actions",
	"curate": "Curate",
	"curation": "Curation",
	"edit": "Edit",
	"guest": "Guest",
	"defaultModeSwitchTitle": "Switch to metadata view",
	"today": "Today",
	"yesterday": "Yesterday",
	"lastWeek": "Last Week",
	"lastMonth": "Last Month",
	"live": "Live",
	"secondAgo": "Second Ago",
	"secondsAgo": "Seconds Ago",
	"minuteAgo": "Minute Ago",
	"minutesAgo": "Minutes Ago",
	"paused": "Paused",
	"hourAgo": "Hour Ago",
	"hoursAgo": "Hours Ago",
	"dayAgo": "Day Ago",
	"daysAgo": "Days Ago",
	"weekAgo": "Week Ago",
	"weeksAgo": "Weeks Ago",
	"metadataModeSwitchTitle": "Return to default view",
	"monthAgo": "Month Ago",
	"monthsAgo": "Months Ago",
	"moreExpand": "more (expand)",
	"moreItems": "more items",
	"more": "More",
	"loading": "Loading...",
	"updating": "Updating...",
	"waiting": "Building view (This may take a moment)...",
	"new": "new",
	"emptyStream": "No items at this time...",
	"sharedThisOn": "I shared this on {service}...",
	"editBtn": "Edit",
	"replyBtn": "Reply",
	"flagBtn": "Flag",
	"unflagBtn": "Unflag",
	"likeBtn": "Like",
	"likeThis": " like this.",
	"likesThis": " likes this.",
	"unlikeBtn": "Unlike",
	"approveBtn": "Approve",
	"removeBtn": "Delete",
	"spamBtn": "Spam",
	"processingFlag": "Flagging...",
	"processingUnflag": "Unflagging...",
	"processingLike": "Liking...",
	"processingUnlike": "Unliking...",
	"changingStatusToCommunityFlagged": "Flagging...",
	"changingStatusToModeratorApproved": "Approving...",
	"changingStatusToModeratorDeleted": "Deleting...",
	"changingStatusToModeratorFlagged": "Marking as spam...",
	"queries": "Queries",
	"processing": "Processing...",
	"userID": "User ID:",
	"userIP": "User IP:",
	"fromLabel": "from",
	"viaLabel": "via"
});

Echo.Item = function(data) {
	this.blocked = false;
	this.selected = false;
	this.controls = {};
	this.init(data);
	this.contextId = data.contextId;
	this.calcAge();
};

Echo.Item.prototype = new Echo.Object();

Echo.Item.prototype.cssPrefix = "echo-item-";

Echo.Item.prototype.template = function() {
return '<div class="echo-item-content">' +
	'<div class="echo-item-container">' +
		'<div class="echo-item-avatar-wrapper">' +
			'<div class="echo-item-avatar"></div>' +
			'<div class="echo-item-status"></div>' +
		'</div>' +
		'<div class="echo-item-wrapper">' +
			'<div class="echo-item-modeSwitch echo-clickable"></div>' +
			'<div>' +
				'<span class="echo-item-authorName echo-linkColor">{Data:actor.title}</span>' +
				'<br>' +
				'<div class="echo-item-data">' +
					'<span class="echo-item-re"></span>' +
					'<span class="echo-item-body echo-primaryColor"></span>' +
					'<br>' +
					'<div class="echo-item-markers echo-secondaryFont echo-secondaryColor"></div>' +
					'<div class="echo-item-tags echo-secondaryFont echo-secondaryColor"></div>' +
					'<div class="echo-item-likes"></div>' +
				'</div>' +
				'<div class="echo-item-metadata">' +
					//XXX: we should show valid IP address. Currently user IP container is hidden.
					'<div class="echo-item-metadata-userIP">' +
						'<span class="echo-item-metadata-title echo-item-metadata-icon echo-item-metadata-userIP">' +
							'{Label:userIP}' +
						'</span>' +
						'<span class="echo-item-metadata-value">127.0.0.1</span>' +
					'</div>' +
					'<div class="echo-item-metadata-userID">' +
						'<span class="echo-item-metadata-title echo-item-metadata-icon echo-item-metadata-userID">' +
							'{Label:userID}' +
						'</span>' +
						'<span class="echo-item-metadata-value">{Data:actor.id}</span>' +
					'</div>' +
				'</div>' +
				'<div class="echo-secondaryColor echo-secondaryFont">' +
					'<div class="echo-item-sourceIcon echo-clickable"></div>' +
					'<div class="echo-item-date"></div>' +
					'<div class="echo-item-from"></div>' +
					'<div class="echo-item-via"></div>' +
					'<div class="echo-item-controls"></div>' +
					'<div class="echo-clear"></div>' +
				'</div>' +
			'</div>' +
		'</div>' +
		'<div class="echo-item-childrenMarker"></div>' +
		'<div class="echo-clear"></div>' +
	'</div>' +
'</div>';
};

Echo.Item.prototype.renderers = {};

Echo.Item.prototype.renderers.markers = function(element, dom) {
	this.render("info", element, dom, {"type": "markers"});
};

Echo.Item.prototype.renderers.tags = function(element, dom) {
	this.render("info", element, dom, {"type": "tags"});
};

Echo.Item.prototype.renderers.info = function(element, dom, extra) {
	var self = this;
	var type = (extra || {}).type;
	if (!this.data.object[type] || !this.isCurationMode()) {
		$(element).remove();
		return;
	}
	var items = $.foldl([], this.data.object[type], function(item, acc){
		var template = (item.length > self.config.get("limits." + type))
			? "<span title={Data:item}>{Data:truncatedItem}</span>"
			: "<span>{Data:item}</span>";
		var truncatedItem = item.substring(0, self.config.get("limits." + type)) + "...";
		acc.push(self.substitute(template, {"item": item, "truncatedItem": truncatedItem}));
	});
	$(element).prepend(items.sort().join(", "));
};

Echo.Item.prototype.renderers.likes = function(element, dom) {
	if (!this.data.object.likes.length || !this.config.get("like")) {
		$(element).hide();
		return;
	}
	var likesPerPage = 5;
	var visibleUsersCount = this.likesUserList
		? this.likesUserList.getVisibleUsersCount()
		: likesPerPage;
	var youLike = false;
	var users = $.map(this.data.object.likes, function(like) {
		if (like.actor.id == Echo.User.get("id")) {
			youLike = true;
		}
		return like.actor;
	});
	this.likesUserList = new Echo.UsersList(element, {
		"data": users,
		"itemsPerPage": likesPerPage,
		"suffixText": Echo.Localization.getLabel(users.length > 1 || youLike ? "likeThis" : "likesThis"),
		"totalUsersCount": this.data.object.accumulators.likesCount,
		"visibleUsersCount": visibleUsersCount
	});
	$(element).show();
};

Echo.Item.prototype.renderers.container = function(element, dom) {
	var self = this;
	var threadSuffix = (this.threading && this.children.length) ||
				(this.replyForm && this.replyForm.visible)
					? '-thread' : '';
	if (this.depth) {
		$(element).addClass('echo-item-container-child' + threadSuffix);
		$(element).addClass('echo-trinaryBackgroundColor');
	} else {
		$(element).addClass('echo-item-container-root' + threadSuffix);
	}
	$(element).addClass('echo-item-depth-' + this.depth);
	var switchClasses = function(action) {
		$.each(self.controls, function(key, control) {
			control[action + "Class"]("echo-linkColor");
		});
	};
	var modeSwitch = $(dom.get("modeSwitch"));
	$(element).bind({
		"mouseleave": function() {
			if (self.isCurationMode()) modeSwitch.hide();
			switchClasses("remove");
		},
		"mouseenter": function() {
			if (self.isCurationMode()) modeSwitch.show();
			switchClasses("add");
		}
	});
};

Echo.Item.prototype.renderers.modeSwitch = function(element) {
	var self = this;
	if (!this.isCurationMode()) return;
	var mode = "default";
	var setTitle = function(el) {
		$(el).attr("title", Echo.Localization.getLabel(mode + "ModeSwitchTitle"));
	};
	setTitle(element);
	$(element)
		.click(function() {
			mode = (mode == "default" ? "metadata" : "default");
			setTitle(this);
			$(self.dom.get("data")).toggle();
			$(self.dom.get("metadata")).toggle();
		});
};

Echo.Item.prototype.renderers.wrapper = function(element) {
	$(element).addClass('echo-item-wrapper' + (this.depth ? '-child' : '-root'));
};

Echo.Item.prototype.renderers.avatar = function() {
	var size = (!this.depth ? 48 : 24);
	var avatar = this.data.actor.avatar || Echo.User.get("defaultAvatar");
	return $('<img src="' + $.escapeURI(avatar) + '" width="' + size + '" height="' + size + '">').get(0);
};

Echo.Item.prototype.renderers.status = function(container) {
	var self = this;
	if (!this.isCurationMode()) {
		$(container).hide();
		return;
	}
	if (this.depth) {
		$(container).addClass('echo-item-status-child');
	}
	var status = this.data.object.status || "Untouched";
	var template =
		'<div class="echo-item-sub-wrapper">' +
			'<input type="checkbox" class="echo-item-status-checkbox">' +
			'<div class="echo-item-status-icon" title="' + Echo.Localization.getLabel("state" + status) + '"></div>' +
			'<div class="echo-clear"></div>' +
		'</div>';
	var element = $(template).addClass('echo-item-status-' + status);
	var checkbox = element.children(".echo-item-status-checkbox");
	checkbox.attr("checked", this.selected);
	checkbox.click(function() {
		self.selected = !self.selected;
		self.publish("Item.onSelect", {
			"unique": self.unique,
			"selected": self.selected
		});
	});
	element.children(".echo-item-status-icon").addClass('echo-item-status-icon-' + status);
	$(container).empty().append(element.get(0));
};

Echo.Item.prototype.renderers.control = function(element, dom, extra) {
	if (!extra || !extra.name) return;
	var template =
		'<a class="echo-item-control echo-item-control-' + extra.name + '">' +
			'{Label:' + extra.name + 'Btn}{Data:count}' +
		'</a>';
	var data = {"count": extra.count || ""};
	return $(this.substitute(template, data))[extra.onetime ? "one" : "bind"]({
		"click": function(event) {
			event.stopPropagation();
			if (extra.callback) extra.callback();
		}
	});
};

Echo.Item.prototype.renderers.replyControl = function(element, dom) {
	var self = this;
	if (this.depth || !this.config.get("reply")) return;
	return this.render("control", element, dom, {
		"name": "reply",
		"callback": function() {
			self.publish("Item.onReply", {
				"unique": self.unique,
				"target": self.dom.content
			});
		}
	});
};

Echo.Item.prototype.renderers.flagControl = function(element, dom) {
	if (!this.config.get("communityFlag")) return;
	var count = this.data.object.flags.length;
	return this.render("userReactionControl", element, dom, {
		"type": "flags",
		"count": this.isCurationMode() && count ? " (" + count + ")" : "",
		"actions": ["Flag", "Unflag"]
	});
};

Echo.Item.prototype.renderers.likeControl = function(element, dom) { 
	if (!this.config.get("like") || !Echo.User.logged()) return;
	return this.render("userReactionControl", element, dom, {
		"type": "likes",
		"actions": ["Like", "Unlike"]
	});
};

Echo.Item.prototype.renderers.userReactionControl = function(element, dom, extra) { 
	var self = this;
	var action =
		($.map(this.data.object[extra.type], function(entry) {
			if (Echo.User.hasIdentity(entry.actor.id)) return entry;
		})).length > 0 ? extra.actions[1] : extra.actions[0];
	var actor = {
		"avatar": Echo.User.get("avatar"),
		"id": Echo.User.get("id"),
		"isGuest": !Echo.User.logged(),
		"title": Echo.User.get("name")
	};
	var control = this.render("control", element, dom, {
		"name": action.toLowerCase(),
		"onetime": true,
		"count": extra.count,
		"callback": function() {
			control.empty().append(Echo.Localization.getLabel("processing" + action));
			self.publish("Item.on" + action, {
				"unique": self.unique,
				"actor": actor
			});
		}
	});
	return control;
};

Echo.Item.prototype.renderers.moderatorControl = function(element, dom, extra) {
	var self = this;
	if (!extra || !extra.name || !extra.status || !this.isCurationMode() ||
			this.data.object.status == extra.status) return;
	return this.render("control", element, dom, {
		"name": extra.name,
		"callback": function() {
			self.publish("Item.onStatusChange", {
				"status": extra.status,
				"unique": self.unique
			});
		}
	});
};

Echo.Item.prototype.renderers.approveControl = function(element, dom) {
	return this.render("moderatorControl", element, dom, {
		"name": "approve",
		"status": "ModeratorApproved"
	});
};

Echo.Item.prototype.renderers.removeControl = function(element, dom) {
	return this.render("moderatorControl", element, dom, {
		"name": "remove",
		"status": "ModeratorDeleted"
	});
};

Echo.Item.prototype.renderers.spamControl = function(element, dom) {
	return this.render("moderatorControl", element, dom, {
		"name": "spam",
		"status": "ModeratorFlagged"
	});
};

Echo.Item.prototype.renderers.editControl = function(element, dom) {
	var self = this;
	if (!this.isCurationMode()) return;
	return this.render("control", element, dom, {
		"name": "edit",
		"callback": function() {
			self.publish("Item.onEdit", {
				"unique": self.unique
			});
		}
	});
};

Echo.Item.prototype.renderers.controls = function(target) {
	var self = this;
	var controls = ["flag", "like", "reply", "approve", "remove", "spam", "edit"];
	var container = $('<div class="echo-item-controls-wrapper"></div>');
	$.map(controls, function(name) {
		var control = self.render(name + "Control");
		if (control) {
			self.controls[name] = control;
			container.append("<span> \u00b7 </span>").append(control);
		}
	});
	$(target).empty().append(container);
};

Echo.Item.prototype.renderers.re = function() {
	if (!this.config.get("reTag")) return;
	var self = this;
	var context = this.data.object.context;
	var re = "";
	//XXX use normalized permalink and location instead
	var permalink = this.data.object.permalink;

	var reOfContext = function(c) {
		var maxLength = self.config.get("limits.reTitle");
		if (!c.title) {
			maxLength = self.config.get("limits.reLink");
			c.title = c.uri.replace(/^https?:\/\/(.*)/ig, '$1');
		}
		if (c.title.length > maxLength) {
			c.title = c.title.substring(0, maxLength) + "...";
		}
		return self.hyperlink({
			"class": "echo-primaryColor",
			"href": c.uri,
			"caption": "Re: " + $.stripTags(c.title)
		}, {
			"openInNewWindow" :self.config.get("openLinksInNewWindow")
		}) + "<br/>";
	}

	var here = document.location.href;
	var here_domain = $.getDomain(here);

	if ((permalink == here) || this.depth || !context || !context.length) {
		return;
	}
	var must_skip_context = false;
	$.each(context, function(i, c) {
		//XXX use normalized uri
		if (c.uri == here) {
			must_skip_context = true;
			return false; //break
		}
	});

	if (must_skip_context) return;

	if (this.config.get("optimizedContext")) {
		var primaryContext = context[0];
		$.each(context, function(i, c) {
			if ($.getDomain(c.uri) == here_domain) {
				primaryContext = c;
				return false; //break
			}
		});
		if (primaryContext) re = reOfContext(primaryContext);
	} else {
		$.each(context, function(i, c) {
			re += reOfContext(c);
		});
	}

	return $("<span>" + re + "</span>").get(0);
};

Echo.Item.prototype.renderers.sourceIcon = function(element) {
	if (!this.config.get("viaLabel.icon") ||
		this.data.source.name == "jskit" ||
		this.data.source.name == "echo") {
			$(element).remove();
	}
	$(element)
		.css('backgroundImage',
			'url(' + $.escapeURI(
					this.data.source.icon ||
					this.data.provider.icon ||
					this.config.get("defaultProviderIcon")
			) + ')'
		)
		.wrap(this.hyperlink({
			"href": this.data.source.uri || this.data.object.permalink
		}, {
			"openInNewWindow": this.config.get("openLinksInNewWindow")
		}));
};

Echo.Item.prototype.renderers.via = function(element, dom) {
	var self = this;
	var get = function(field) {
		return (self.data[field].name || "").toLowerCase();
	};
	if (get("source") == get("provider")) return;
	this.render("viaText", element, dom, {
		"label": "via",
		"field": "provider"
	});
};

Echo.Item.prototype.renderers.from = function(element, dom) {
	this.render("viaText", element, dom, {
		"label": "from",
		"field": "source"
	});
};

Echo.Item.prototype.renderers.viaText = function(element, dom, extra) {
	extra = extra || {};
	var data = this.data[extra.field];
	if (!this.config.get("viaLabel.text") || !data.name || data.name == "jskit"  || data.name == "echo") return;
	var a = this.hyperlink({
		"class": "echo-secondaryColor",
		"href": data.uri || this.data.object.permalink,
		"caption": data.name
	}, {
		"openInNewWindow": this.config.get("openLinksInNewWindow")
	});
	$(element).html('&nbsp;' + Echo.Localization.getLabel(extra.label + 'Label') + '&nbsp;').append(a);
};

Echo.Item.prototype.renderers.body = function(element) {
	var self = this;
	var output = function(text) {
		return $(element).append('<span>' + text + '</span>');
	};
	// temporary fix because Firefox hides CDATA content
	var text = this.data.object.content.replace(/<!\[CDATA\[(.*?)\]\]>/g, '$1');
	var source = this.data.source.name;
	var contentTransformations = this.config.get("contentTransformations");
	if (source && source == "Twitter" && this.config.get("aggressiveSanitization")) {
		output(Echo.Localization.getLabel("sharedThisOn", {"service": source}));
		return;
	}
	if (contentTransformations.smileys) {
		var f = function(v) { return v.replace(/([\W])/g, "\\$1"); };
		var smileTag = function(smile) {
			return '<img src="//js-kit.com/extra/tiny_mce/plugins/emotions/img/smiley-' + smile.file + '" title="' + smile.title + '" border="0" alt="' + smile.title + '" />';
		};
		var smiles = {
			'O:-)'		: {file: 'innocent.gif', title: 'Innocent'},
			'&gt;:o'	: {file: 'yell.gif', title: 'Yell'},
			':)'		: {file: 'smile.gif', title: 'Smile'},
			':-)'		: {file: 'smile.gif', title: 'Smile'},
			';)'		: {file: 'wink.gif', title: 'Wink'},
			';-)'		: {file: 'wink.gif', title: 'Wink'},
			':\'('		: {file: 'cry.gif', title: 'Cry'},
			'8-)'		: {file: 'cool.gif', title: 'Cool'},
			':('		: {file: 'frown.gif', title: 'Frown'},
			':-('		: {file: 'frown.gif', title: 'Frown'},
			':*'		: {file: 'kiss.gif', title: 'Kiss'},
			':-*'		: {file: 'kiss.gif', title: 'Kiss'},
			':-D'		: {file: 'laughing.gif', title: 'Laughing'},
			'=-O'		: {file: 'surprised.gif', title: 'Surprised'},
			'=-X'		: {file: 'sealed.gif', title: 'Sealed'},
			':-['		: {file: 'embarassed.gif', title: 'Embarassed'},
			':-$'		: {file: 'money-mouth.gif', title: 'Money mouth'},
			':-P'		: {file: 'tongue-out.gif', title: 'Tongue out'},
			':-E'		: {file: 'foot-in-mouth.gif', title: 'Foot in mouth'},
			'*DONT_KNOW*'	: {file: 'undecided.gif', title: 'Undecided'}
		};
		$.each(smiles, function(i, el) {
			text = text.replace(new RegExp(f(i), 'g'), smileTag(el));
		});
	}

	var wrap = function(tag) {
		var template = 
			(tag.length > self.config.get("limits.tags"))
			? '<span class="echo-item-tag" title={Data:tag}>{Data:truncatedTag}</span>'
			: '<span class="echo-item-tag">{Data:tag}</span>';
		var truncatedTag = tag.substring(0, self.config.get("limits.tags")) + "...";
		return (self.substitute(template, {"tag": tag, "truncatedTag": truncatedTag}));	
	};

	if (contentTransformations.hashtags) {
		text = text.replace(/(#|\uff03)(<a[^>]*>[^<]*<\/a>)/ig, function($0, $1, $2){
			return wrap($2);
		});
	}

	var insertHashTags = function(t) {
		if (!contentTransformations.hashtags) return t;
		return t.replace(/(^|[^0-9a-z&\/]+)(?:#|\uff03)([0-9a-z_]*[a-z_]+[a-z0-9_\u00c0-\u00d6\u00d8-\u00de\u00e0-\u00f6\u00f8-\u00ff]*)/ig, function($0, $1, $2) {
			return $1 + wrap($2);
		});
	};
	var tags;
	var tags2meta = function(t) {
		tags = [];
		t = t.replace(/((<a\s+[^>]*>)(.*?)(<\/a>))|<.*?>/ig, function($0, $1, $2, $3, $4) {
			if ($1) $0 = $2 + insertHashTags($3) + $4;
			tags.push($0);
			return ' %%HTML_TAG%% ';
		});
		return t;
	};
	var meta2tags = function(t) {
		$.each(tags, function(i, v) {
			t = t.replace(' %%HTML_TAG%% ', v);
		});
		return t;
	};
	var urlMatcher = "((?:http|ftp|https):\\/\\/(?:[a-z0-9#:\\/\\;\\?\\-\\.\\+,@&=%!\\*\\'(){}\\[\\]$_|^~`](?!gt;|lt;))+)";
	text = tags2meta(text);
	if (source && source != 'jskit' && source != 'echo') {
		var url = this.depth
			? this.data.target.id
			: this.config.get("reTag")
				? this.data.object.permalink || this.data.target.id
				: undefined;
		if (url) {
			text = text.replace(new RegExp(url || "", "g"), "");
			if (!/\S/.test(text)) {
				output(Echo.Localization.getLabel("sharedThisOn", {"service": source}));
				return;
			}
		}
	}
	text = insertHashTags(text);
	if (contentTransformations.urls) {
		text = text.replace(new RegExp(urlMatcher, 'ig'), function($0, $1) {
			return self.hyperlink({
				'href': $1,
				'caption': $1
			}, {
				'skipEscaping': true,
				'openInNewWindow': self.config.get("openLinksInNewWindow")
			});
		})
	}

	if (contentTransformations.newlines) {
		text = text.replace(/\n\n+/g, '\n\n');
	}
	// TODO: remove the lines below
	if (this.config.get("replaceNewlines")) {
		text = text.replace(/\n/g, '&nbsp;<br />');
	}
	text = meta2tags(text);
	if (contentTransformations.urls) {
		text = text.replace(new RegExp("(<a\\s+[^>]*>)" + urlMatcher + "(<\\/a>)", "ig"), function($0, $1, $2, $3) {
			if ($2.length > self.config.get("limits.bodyLink")) {
				return $1 + $2.substring(0, self.config.get("limits.bodyLink")) + "..." + $3;
			}
			return $0;
		});
	}
	if (this.config.get("limits.body")) {
		text = $.htmlTextTruncate(text, this.config.get("limits.body"), "...");
	}
	output(text);
};

Echo.Item.prototype.renderers.date = function(element) {
	var el = element || this.dom && this.dom.get("date");
	if (el) {
		$(el).html(this.age);
	}
};

Echo.Item.prototype.changeItemStatus = function(status) {
	var self = this;
	this.selected = false;
	this.data.object.status = status;
	$.each(["status", "controls"], function(id, area) {
		self.rerender(area);
	});
};

Echo.Item.prototype.calcAge = function() {
	if (!this.timestamp) return;
	var d = new Date(this.timestamp * 1000);
	var now = (new Date()).getTime();
	var when;
	var diff = Math.floor((now - d.getTime()) / 1000);
	var day_diff = Math.floor(diff / 86400);

	function getAgo(ago, period) {
		var diff = '';
		if (1 == (ago % 10) && (11 != ago)) {
			diff = period + "Ago";
		} else {
			diff = period + "sAgo";
		}
		return ago + ' ' + Echo.Localization.getLabel(diff);
	}

	if (isNaN(day_diff) || day_diff < 0 || day_diff >= 365) {
		when = d.toLocaleDateString() + ', ' + d.toLocaleTimeString();
	} else if (diff < 60) {
		when = getAgo(diff, 'second');
	} else if (diff < 60 * 60) {
		diff = Math.floor(diff / 60);
		when = getAgo(diff, 'minute');
	} else if (diff < 60 * 60 * 24) {
		diff = Math.floor(diff / (60 * 60));
		when = getAgo(diff, 'hour');
	} else if (diff < 60 * 60 * 48) {
		when = Echo.Localization.getLabel("yesterday");
	} else if (day_diff < 7) {
		when = getAgo(day_diff, 'day');
	} else if (day_diff < 14) {
		when = Echo.Localization.getLabel("lastWeek");
	} else if (day_diff < 30) {
		diff =  Math.floor(day_diff / 7);
		when = getAgo(diff, 'week');
	} else if (day_diff < 60) {
		when = Echo.Localization.getLabel("lastMonth");
	} else if (day_diff < 365) {
		diff =  Math.floor(day_diff / 31);
		when = getAgo(diff, 'month');
	}
	if (this.age != when) {
		this.age = when;
	}
};

Echo.Item.prototype.like = function(actor) {
	this.userReaction("likes", actor.id, [{"actor": actor}]);
};

Echo.Item.prototype.unlike = function(actor) {
	this.userReaction("likes", actor.id, []);
};

Echo.Item.prototype.flag = function(actor) {
	this.userReaction("flags", actor.id, [{"actor": actor}]);
};

Echo.Item.prototype.unflag = function(actor) {
	this.userReaction("flags", actor.id, []);
};

Echo.Item.prototype.userReaction = function(type, actorID, container) {
	this.data.object[type] = $.foldl(container, this.data.object[type], function(entry, acc) {
		if (entry.actor.id != actorID) acc.push(entry);
	});
	this.rerender(type);
	this.rerender("controls");
};

Echo.Item.prototype.block = function(label) {
	if (this.blocked) return;
	this.blocked = true;
	var content = $(this.dom.get("content"));
	var width = content.width();
	var height = content.height();
	this.blockers = {
		"backdrop": $('<div class="echo-item-blocker-backdrop"></div>').css({
			"width": width, "height": height
		}),
		"message": $(this.substitute('<div class="echo-item-blocker-message">{Data:label}</div>', {"label": label})).css({
			"left": ((parseInt(width) - 200)/2) + 'px',
			"top": ((parseInt(height) - 20)/2) + 'px'
		})
	};
	content.addClass("echo-relative")
		.prepend(this.blockers.backdrop)
		.prepend(this.blockers.message);
};

Echo.Item.prototype.unblock = function() {
	if (!this.blocked) return;
	this.blocked = false;
	this.blockers.backdrop.remove();
	this.blockers.message.remove();
	$(this.dom.get("content")).removeClass("echo-relative");
};

Echo.Item.prototype.getAccumulator = function(type) {
	return this.data.object.accumulators[type];
}; 

Echo.Item.prototype.isCurationMode = function() {
	return Echo.User.logged() && Echo.User.hasRole(["administrator", "moderator"]);
};
 
Echo.ReplyForm = function(config) {
	this.visible = true;
	this.config = config || {};
};

Echo.ReplyForm.prototype = new Echo.Object();

Echo.ReplyForm.prototype.cssPrefix = "echo-reply-form-";

Echo.ReplyForm.prototype.renderers = {};

Echo.ReplyForm.prototype.template = '<div class="echo-reply-form-container echo-item-container echo-item-container-child echo-trinaryBackgroundColor echo-item-depth-1"></div>';

Echo.ReplyForm.prototype.renderers.container = function(element) {
	$(element).click(function(event) {
		event.stopPropagation();
	});
	this.form = new Echo.Submit(element, this.config);
};

Echo.ReplyForm.prototype.text = function() {
	return this.form && this.form.dom && $(this.form.dom.get("text")).val();
};

Echo.ReplyForm.prototype.expand = function() {
	this.form.config.set("mode", this.getMode() == "standard" ? "compact" : "standard");
	this.form.rerender();
};

Echo.ReplyForm.prototype.toggle = function() {
	this.visible = !this.visible;
	$(this.dom.content).toggle();
};

Echo.ReplyForm.prototype.show = function(mode) {
	if (!this.visible && this.form) {
		if (mode != this.getMode()) {
			this.form.config.set("mode", mode);
			this.form.rerender();
		}
		this.visible = true;
		$(this.dom.content).show();
	}
};

Echo.ReplyForm.prototype.hide = function() {
	if (this.visible && this.form) {
		this.visible = false;
		$(this.dom.content).hide();
	}
};

Echo.ReplyForm.prototype.remove = function() {
	$(this.dom.content).remove();
};

Echo.ReplyForm.prototype.getMode = function() {
	return this.form && this.form.config.get("mode");
};

Echo.Whirlpool = function(data) {
	this.init(data);
	this.data.count = this.data.items.length;
};

Echo.Whirlpool.prototype = new Echo.Object();

Echo.Whirlpool.prototype.cssPrefix = "echo-whirlpool-";

Echo.Whirlpool.prototype.renderers = {};

Echo.Whirlpool.prototype.template = function() {
	var label = "more" + (this.config.get("truncateReplies.clickable") ? "Expand" : "Items");
	var template = 
	'<div class="echo-whirlpool-container echo-trinaryBackgroundColor echo-item-depth-1 echo-item-container-child">' +
		'<span class="echo-whirlpool-message">{Data:count} {Label:' + label + '}</span>' +
	'</div>';
	return template;
};

Echo.Whirlpool.prototype.renderers.container = function(element) {
	var self = this;
	if (this.config.get("truncateReplies.clickable")) {
		$(element).addClass("echo-clickable")
			.click(function() { self.onexpand(self.data.items); });
	}
	if (this.config.get("truncateReplies.after") == 0) {
		$(element).addClass(this.cssPrefix + "container-collapse-all");
	}
};

Echo.Whirlpool.prototype.renderers.message = function(element) {
	if (this.config.get("truncateReplies.clickable")) {
		$(element).addClass("echo-linkColor");
	}
};

Echo.StreamClient = function(target, config) {
	if (!target) return;
	var self = this;
	this.target = target;
	this.init();
	this.config = new Echo.Config(config || {}, {
		"appkey": "",
		"aggressiveSanitization": false,
		"checkViewTimeout": 2,
		"children": {
			"depth": 2,
			"sortOrder": "chronological"
		},
		"childrenMaxDepth": 1,
		"communityFlag": false,
		"contentTransformations": {
			"text": ["smileys", "hashtags", "urls", "newlines"],
			"html": ["smileys", "hashtags", "urls", "newlines"],
			"xhtml": ["smileys", "hashtags", "urls"]
		},
		"fadeTimeout": 3500,
		"flashColor": "#ffff99",
		"itemsPerPage": 15,
		"like": false,
		"liveUpdates": true,
		"liveUpdatesTimeout": 10,
		"maxBodyLinkLength": 50,
		"maxBodyCharacters": undefined,
		"maxReLinkLength": 30,
		"maxReTitleLength": 143,
		"maxTagLength": 16,
		"maxMarkerLength": 16,
		"openLinksInNewWindow": false,
		"optimizedContext": true,
		"providerIcon": "http://js-kit.com/favicon.ico",
		"replaceNewlines": true, //TODO temporary parameter for A:1371 bugfix.
		"reply": false,
		"replyActionString": undefined,
		"replySubmitPermissions": undefined,
		"reTag": true,
		"slideTimeout": 700,
		"sortOrder": "reverseChronological",
		"streamStateLabel": {
			"icon": true,
			"text": true
		},
		"submissionProxyURL": window.location.protocol + "//js-kit.com/apps/esp/activity",
		"truncateReplies": {
			"after": 2,
			"clickable": true
		},
		"uri": "http://api.js-kit.com",
		"viaLabel": {
			"icon": false,
			"text": false
		}
	}, this.normalizeConfigValue);
	this.states = [
		"Untouched",
		"ModeratorApproved",
		"ModeratorDeleted",
		"CommunityFlagged",
		"ModeratorFlagged",
		"SystemFlagged"
	];
	this.addCss();
	this.contextId = this.newContextId();
	$(this.target).empty().append(this.render());
	if (!this.initApplication()) return;
	Echo.User.init(function() {
		self.recalcEffectsTimeouts();
		self.initialItemsRequest();
		self.listenBroadcastMessages();
		self.publish("Stream.onRender", self.prepareBroadcastParams());
	});
};

Echo.StreamClient.prototype = new Echo.Application();

Echo.StreamClient.prototype.cssPrefix = "echo-stream-";

Echo.StreamClient.prototype.template = 
	'<div class="echo-stream-container echo-primaryFont echo-primaryBackgroundColor">' +
		'<div class="echo-stream-header">'+
			'<div class="echo-stream-curate echo-linkColor"></div>' +
			'<div class="echo-stream-state echo-secondaryColor"></div>' +
			'<div class="echo-clear"></div>' +
		'</div>' +
		'<div class="echo-stream-body"></div>' +
		'<div class="echo-stream-more"></div>' +
		'<div class="echo-stream-brand">'+
			'<a class="echo-stream-brand-link" href="http://js-kit.com" target="_blank">' +
				'<div class="echo-stream-brand-message">social networking by</div>' +
			'</a>' +
		'</div>' +
	'</div>';

Echo.StreamClient.prototype.renderers = {};

Echo.StreamClient.prototype.renderers.body = function(element) {
	var labelName = 'loading';
	if (this.error && this.error.errorCode == 'waiting') {
		labelName = 'waiting';
	}
	this.showMessage({
		type: 'loading',
		message: Echo.Localization.getLabel(labelName)
	}, element);
};

Echo.StreamClient.prototype.renderers.state = function(element) {
	var status = this.animation.paused ? 'paused' : 'live';
	element = $(element || this.dom.get("state"));
	var templates = {
		"picture" : '<span class="echo-stream-state-picture echo-stream-state-picture-' + status +'"></span>',
		"message" : '<span class="echo-stream-state-message">{Label:' + status + '}</span>',
		"count" : '<span class="echo-stream-state-count">({Data:count} {Label:new})</span>'
	};
	element.empty();
	if (this.config.get("streamStateLabel.icon")) {
		element.append(templates.picture);
	}
	if (this.config.get("streamStateLabel.text")) {
		element.append(this.substitute(templates.message));
		var entries = $.foldl([], this.animation.queue, function(entry, acc) {
			if (entry.affectCounter) {
				acc.push(entry);
			}
		});
		if (entries.length > 0 && this.animation.paused) {
			element.append(this.substitute(
				templates.count,
				{"count": entries.length}
			));
		}
	}
};

Echo.StreamClient.prototype.renderers.curate = function(container) {
	var self = this;
	var element = $(container || this.dom.get("curate"));
	if (!this.isCurationMode() || !Echo.QueryPalette) {
		element.hide();
		return;
	}
	element
		.empty()
		.append($(this.substitute('<span class="echo-stream-curate-label">{Label:curate}</span>')))
		.show()
		.click(function() {
			self.assembleCurationDialog();
			self.curation.dialog.open();
		});
};

Echo.StreamClient.prototype.init = function() {
	this.animation = {
		"queue": [],
		"paused": false,
		"initiated": false
	};
	this.curation = {
		"queue": []
	};
	this.items = {};   // items by unique key hash
	this.threads = []; // items tree
	if (this.checkViewTimer) {
		clearTimeout(this.checkViewTimer);
	}
};

Echo.StreamClient.prototype.listenBroadcastMessages = function() {
	var self = this;
	this.subscribe("User.onInvalidate", function() {
		self.refresh();
	});
	this.subscribe("Item.onStatusChange", function(event, data) {
		var item = self.items[data.unique];
		item.block(Echo.Localization.getLabel("changingStatusTo" + data.status));
		$.get(self.config.get("submissionProxyURL"), {
			"appkey": self.config.get("appkey"),
			"content": $.object2JSON({
				"verb": "update",
				"target": item.id,
				"author": item.data.actor.id,
				"field": "state",
				"value": data.status
			}),
			"sessionID": Echo.User.get("sessionID", "")
		}, function(data) {
			if (data.result == "error") {
				item.unblock();
			} else {
				item.changeItemStatus(data.status);
				self.liveUpdatesRequest();
			}
		}, "jsonp");
	});
	this.subscribe("Item.onSelect", function(event, data) {
		var item = self.items[data.unique];
		if (data.selected) {
			self.curation.queue.push(item);
			self.assembleCurationDialog();
			self.curation.dialog.open();
			self.curation.tabs.select("actions");
		} else {
			self.curation.queue = $.foldl([], self.curation.queue,
				function(element, acc) {
					if (element.unique != data.unique) acc.push(element);
				});
		}
		if (self.curation.bulk) {
			self.curation.bulk.refresh(self.curation.queue);
		}
	});
	this.subscribe("BulkActions.onStatusChange", function(event, data) {
		self.applyBulkStatusChanges(data.state)
	});
	if (this.isReplyFormActive()) {
		var prepareParams = function(item) {
			return self.prepareBroadcastParams({
				"form": item.replyForm.form,
				"item": {
					"data": item.data,
					"target": item.dom.content
				}
			});
		};
		this.subscribe("Item.onReply", function(event, data) {
			var item = self.items[data.unique];
			if (!item) return;
			self.publish("Stream.Item.onControlClick", self.prepareBroadcastParams({
				"name": "reply",
				"item": {
					"data": item.data,
					"target": item.dom.content
				}
			}));
			if (item.replyForm) {
				if (item.children.length) {
					item.replyForm.expand();
				} else {
					item.replyForm.toggle();
				}
			} else {
				item.replyForm = self.initReplyForm(item, "standard");
				$(data.target).after(item.replyForm.render());
			}
			var action = item.replyForm.visible &&
				item.replyForm.getMode() == "standard" ? "Expand" : "Collapse";
			self.publish("Stream.Plugins.Reply.onForm" + action, prepareParams(item));
			if (!item.children.length) {
				item.rerender();
			}
			item.replyForm.dom.content.scrollIntoView();
		});
		this.subscribe("Submit.onExpand", function(event, args) {
			var item = self.items[args.data.unique];
			if (!item || !item.replyForm) return;
			self.publish("Stream.Plugins.Reply.onFormExpand", prepareParams(item));
		});
		$(document).click(function() {
			self.publish("document.onclick");
		});
	}
	if (this.isSubmitFormActive()) {
		this.subscribe("Item.onEdit", function(event, data) {
			var item = self.items[data.unique];
			if (!item) return;
			var data = item.data;
			data.unique = item.unique;
			item.editPopup = new Echo.UI.Dialog({
				"content": function(target) {
					$(target).addClass("echo-edit-item-container");
					var form = new Echo.Submit(target, {
						"data": data,
						"mode": "edit",
						"adminMode": true,
						"targetURL": item.id,
						"appkey": self.config.get("appkey"),
						"contextId": self.contextId
					});
				},
				"config": {
					"autoOpen": true,
					"title": Echo.Localization.getLabel("edit"),
					"width": 400,
					"height": 320,
					"minWidth": 300,
					"minHeight": 320
				}
			});
		});
		this.subscribe("Submit.onEditInit", function(event, args) {
			var item = self.items[args.data.unique];
			if (!item || !item.editPopup) return;
			item.block(Echo.Localization.getLabel("updating"));
		});
		this.subscribe("Submit.onEditComplete", function(event, args) {
			var item = self.items[args.data.unique];
			if (!item || !item.editPopup) return;
			item.editPopup.close();
		});
	}
	$.map(["Like", "Unlike", "Flag", "Unflag"], function(name) {
		self.subscribe("Item.on" + name, function(event, data) {
			var item = self.items[data.unique];
			if (!item) return;
			self.publish("Stream.Item.onControlClick", self.prepareBroadcastParams({
				"name": name.toLowerCase(), 
				"item": {
					"data": item.data,
					"target": item.dom.content
				}
			}));
			$.get(self.config.get("submissionProxyURL"), {
				"appkey": self.config.get("appkey"),
				"content": $.object2JSON({
					"verb": name.toLowerCase(),
					"target": item.id
				}),
				"sessionID": Echo.User.get("sessionID", "")
			}, function() {
				var item = self.items[data.unique];
				var action = name.toLowerCase();
				var plugin = action.match(/like/) ? "Like": "CommunityFlag";
				item[action](data.actor);
				self.publish("Stream.Plugins." + plugin + ".on" + name + "Complete",
					self.prepareBroadcastParams({
						"item": {
							"data": item.data,
							"target": item.dom.content
						}
					}));
			}, "jsonp");
		});
	});
	$.map(["Submit.onPostComplete", "Submit.onEditComplete"], function(topic) {
		Echo.Broadcast.subscribe(topic, function() {
			self.liveUpdatesUnsubscribe();
			self.liveUpdatesRequest();
		});
	});
};

Echo.StreamClient.prototype.initialItemsRequest = function() {
	var self = this;
	this.requestItems({}, function(items, container) {
		if (items.length) {
			container.empty();
			self.appendItems(items, container);
			if (!self.isViewComplete) {
				self.assembleMoreButton($(self.dom.get("more")));
			}
		} else {
			self.showMessage({
				type: 'empty',
				message: Echo.Localization.getLabel('emptyStream')
			}, self.dom.get('body'));
		}
		container.bind({
			"mouseleave": function() {
				self.animation.paused = false;
				if (!self.animation.initiated) {
					self.animateNextItem();
				}
				self.rerender("state");
			},
			"mouseenter": function() {
				self.animation.paused = true;
				self.rerender("state");
			}	
		});
		self.rerender("curate");
		self.publish("Stream.onReady", self.prepareBroadcastParams());
	});
};

Echo.StreamClient.prototype.refresh = function() {
	this.liveUpdatesUnsubscribe();
	this.init();
	this.rerender("body");
	this.initialItemsRequest();
	this.publish("Stream.onRerender", this.prepareBroadcastParams());
};

Echo.StreamClient.prototype.isSubmitFormActive = function() {
	return !!window.Echo.Submit;
};

Echo.StreamClient.prototype.isReplyFormActive = function() {
	return this.config.get("reply") && this.isSubmitFormActive();
};

Echo.StreamClient.prototype.isCurationMode = function() {
	return Echo.User.logged() && Echo.User.hasRole(["administrator", "moderator"]);
};

Echo.StreamClient.prototype.extractPresentationConfig = function(data) {
	return $.foldl({}, ["sortOrder", "itemsPerPage", "safeHTML"], function(key, acc) {
		if (data[key]) acc[key] = data[key];
	});
};

Echo.StreamClient.prototype.extractTimeframeConfig = function(data) {
	var get_comparator = function(value) {
		var m = value.match(/^(<|>)(.*)$/);
		var op = m[1];
		var v = m[2].match(/^'([0-9]+) seconds ago'$/);
		var getTS = v ?
			function() { return Math.floor((new Date()).getTime() / 1000) - v[1] } : function() { return m[2] };
		var f;
		if(op=='<') {
			f = function(ts) {
				return ts < getTS()
			}
		} else if (op=='>') {
			f = function(ts) {
				return ts > getTS()
			}
		}
		return f;
	};
	return {
		"timeframe": $.foldl([], ["before", "after"],
			function(key, acc) {
				if (!data[key]) return;
				var cmp = get_comparator(data[key]);
				if (cmp) acc.push(cmp);
			})
	}
};

Echo.StreamClient.prototype.normalizeConfigValue = function(key, value) {
	var self = this;
	var ensurePositiveValue = function(v) { return v < 0 ? 0 : v; };
	var normalize = {
		"truncateReplies" : function(object) {
			var count = object.after/2;
			return {
				"after": object.after,
				"legacy": Math.floor(count),
				"recent": Math.round(count),
				"clickable": object.clickable
			};
		},
		"contentTransformations" : function(object) {
			$.each(object, function(contentType, options) {
				object[contentType] = $.foldl({}, options || [], function(option, acc) {
					acc[option] = true;
				});
			});
			return object;
		},
		"safeHTML" : function(value) {
			return "off" != value;
		},
		"fadeTimeout": ensurePositiveValue,
		"slideTimeout": ensurePositiveValue
	};
	return normalize[key] ? normalize[key](value) : value;
};

Echo.StreamClient.prototype.getRespectiveAccumulator = function(item, sort) {
	var accBySort = {
		"likesDescending": "likesCount",
		"repliesDescending": "repliesCount"
	};
	return item.getAccumulator(accBySort[sort]);
};

Echo.StreamClient.prototype.appendItems = function(items, container) {
	var self = this;
	$.each(items || [], function(i, item) {
		container.append(item.render());
		// excluding whirlpool and reply form items
		if (item.unique && self.items[item.unique]) {
			self.publish("Stream.Item.onRender", self.prepareBroadcastParams({
				"item": {
					"data": item.data,
					"target": item.dom.content
				}
			}));
		}
	});
};

Echo.StreamClient.prototype.assembleMoreButton = function(element) {
	var self = this;
	var assemble = function(caption) {
		return self.substitute('<span>{Label:' + caption + '}</span>');
	};
	var label = $(assemble("more"));
	var setLabel = function(caption) {
		label.empty().append(assemble(caption));
	};
	var getPageAfter = function() {
		var sort = self.config.get("sortOrder");
		return self.pageAfter
			? (self.pageAfter.timestamp + (
				sort.match(/replies|likes/)
					? "|" + self.getRespectiveAccumulator(self.pageAfter, sort)
					: ""))
			: "0";
	};
	element.empty()
		.append(label)
		.addClass('echo-stream-more-wrapper')
		.show()
		.bind({
			'mouseenter': function() {
				$(this).addClass("echo-stream-more-hover");
			},
			'mouseleave': function() {
				$(this).removeClass("echo-stream-more-hover");
			}
		})
		.unbind('click')
		.one('click', function() {
			self.publish("Stream.onMoreButtonPress", self.prepareBroadcastParams());
			setLabel("loading");
			self.requestItems({
				"pageAfter": '"' + getPageAfter() + '"'
			}, function(items, container) {
				var button = $(self.dom.get("more"));
				if (items.length) {
					self.appendItems(items, container);
					if (self.isViewComplete) {
						button.hide();
					} else {
						self.assembleMoreButton(button);
					}
				} else {
					setLabel("emptyStream");
					button.delay(1000).fadeOut(1000, function() {
						button.hide();
					});
				}
			});
		});
};

Echo.StreamClient.prototype.prepareBroadcastParams = function(params) {
	params = params || {};
	params.target = this.target;
	params.query = this.config.get("query");
	return params;
};

Echo.StreamClient.prototype.assembleCurationDialog = function() {
	var self = this;
	if (this.curation.dialog) return this.curation;
	var assembleQueryPalette = function(target) {
		self.curation.palette = new Echo.QueryPalette(target, {
			"query": {
				"path": self.extractQueryPath(),
				"states": [
					"Untouched",
					"SystemFlagged",
					"CommunityFlagged",
					"ModeratorFlagged"
				],
				"itemsPerPage": self.config.get("itemsPerPage"),
				"sortOrder": self.config.get("sortOrder")
			},
			"appkey": self.config.get("appkey"),
			"contextId": self.contextId
		});
		self.subscribe("QueryPalette.onApply", function(event, query) {
			$(self.dom.get("more")).hide();
			self.config.set("query", query);
			self.refresh();
		});
	};
	var assembleBulkActions = function(target) {
		self.curation.bulk = new Echo.BulkActions(target, {
			"items": self.curation.queue,
			"contextId": self.contextId
		});
	};
	var assembleTabs = function(target) {
		self.curation.tabs = new Echo.UI.Tabs({
			"target": $(target),
			"content": $(target),
			"addUIClass": false,
			"idPrefix": "curation-tabs-",
			"tabs": [{
				"id": "queries",
				"label": Echo.Localization.getLabel("queries"),
				"icon": true,
				"content": assembleQueryPalette
			}, {
				"id": "actions",
				"label": Echo.Localization.getLabel("actions"),
				"icon": true,
				"content": assembleBulkActions
			}]
		});
	};
	this.curation.dialog = new Echo.UI.Dialog({
		"content": assembleTabs,
		"hasTabs": true,
		"config": {
			"autoOpen": false,
			"open": function() {
				self.curation.palette.refresh();
			},
			"title": Echo.Localization.getLabel("curation"),
			"width": 500,
			"height": 550,
			"minWidth": 450,
			"minHeight": 415,
			"maxHeight": 600
		}
	});
	return this.curation;
};

Echo.StreamClient.prototype.applyBulkStatusChanges = function(state) {
	var self = this, ids = [], queue = {};
	$.each(this.curation.queue, function(i, item) {
		item.block(Echo.Localization.getLabel("changingStatusTo" + state));
		ids.push(item.id);
		queue[i] = item;
	});
	this.curation.queue = [];
	if (this.curation.bulk) {
		this.curation.bulk.refresh([]);
	}
	if (!ids.length) return;
	var activities = $.map(ids, function(id, i) {
		return {
			"verb": "update",
			"target": id,
			"author": queue[i].data.actor.id,
			"field": "state",
			"value": state
		};
	});
	$.get(this.config.get("submissionProxyURL"), {
		"appkey": self.config.get("appkey"),
		"content": $.object2JSON(activities),
		"sessionID": Echo.User.get("sessionID", "")
	}, function(data) {
		$.each(queue, function(i, item) {
			if (data.result == "error") {
				item.unblock();
			} else {
				item.changeItemStatus(state);
			}
		});
		self.liveUpdatesRequest();
	}, "jsonp");

};

Echo.StreamClient.prototype.extractQueryPath = function() {
	var path = this.config.get("query").match(/(?:url|scope|childrenof):(\S+)(?: |$)/);
	return path ? path[1] : window.location.protocol + "//" + window.location.host + "/*";
};

Echo.StreamClient.prototype.timestampFromW3CDTF = function(t) {
	var parts = ['year', 'month', 'day', 'hours', 'minutes', 'seconds'];
	var dt = {};
	var matches = t.match(/^(\d{4})-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d)Z$/);
	$.each(parts, function(i, p) {
		dt[p] = matches[i + 1];
	});
	return Date.UTC(dt['year'], dt['month'] - 1, dt['day'],
			dt['hours'], dt['minutes'], dt['seconds']) / 1000;
};

Echo.StreamClient.prototype.constructSearchQuery = function(extra) {
	var self = this;
	var toString = function(value) {
		return typeof(value) == "undefined" ? undefined : value.toString();
	};
	var construct = function(properties, _extra) {
		_extra = _extra || {};
		return $.trim($.foldl([], properties || [], function(key, acc, i) {
			var value = toString(_extra[key]) || toString(self.config.get(key));
			if (value) {
				acc.unshift(key + ":" + value);
			}
		}).join(" "));
	};
	var particles = [
		construct(["pageAfter"], extra),
		this.config.get("query") || ""
	];
	return $.trim(particles.join(" "));
};

Echo.StreamClient.prototype.requestItems = function(extra, visualizator) {
	var self = this;
	this.sendRequest({
		"endpoint": "search",
		"query": {
			"appkey": this.config.get("appkey"),
			"q": this.constructSearchQuery(extra)
		}
	}, function(data) {
		self.handleInitialResponse(data, visualizator);
	});
};

Echo.StreamClient.prototype.sendRequest = function(data, callback) {
	$.get(this.config.get("uri") + "/v1/" + data.endpoint,
		data.query, callback, "jsonp");
};

Echo.StreamClient.prototype.handleErrorResponse = function(data) {
	if (this.dom.get("more")) {
		$(this.dom.get("more")).hide();
	}
	this.liveUpdatesUnsubscribe();
	var messageData = {
		"type": "error",
		"message": data.errorCode + (data.errorMessage ? ": " + data.errorMessage : "")
	};
	if (data.errorCode == 'waiting') {
		messageData = {
			"type": 'loading',
			"message": Echo.Localization.getLabel('waiting')
		};
	}
	this.showMessage(messageData, this.dom.get('body'));
};

Echo.StreamClient.prototype.handleInitialResponse = function(data, visualizator) {
	var self = this, items = [], roots = [];
	data = data || {};
	if (data.result == 'error') {
		if (this.error != data) { //avoid rerendering of the same error message
			this.handleErrorResponse(data);
		}
		this.error = data;
		if (data.errorCode == 'waiting') {
			this.checkViewTimer = setTimeout(function() {
				self.refresh();
			}, this.config.get("checkViewTimeout") * 1000);
		}
		return;
	}

	delete this.error;

	this.nextSince = data.nextSince || 0;
	this.config.extend(this.extractPresentationConfig(data));
	this.config.extend(this.extractTimeframeConfig(data));
	$.each(data.entries || [], function(i, entry) {
		entry = self.normalizeEntry(entry);
		items.push(self.initItem(entry));
	});
	var sortOrder = self.config.get("sortOrder");
	// avoiding problem when children can go before parents
	$.each(items, function(i, item) {
		self.applyStructureUpdates("add", item);
		if (self.isRootItem(item)) {
			item.forceInject = true;
			self.addItemToList(roots, item, sortOrder);
		}
	});

	this.pageAfter = this.threads.length
		? this.threads[this.threads.length - 1]
		: undefined;
	this.isViewComplete = roots.length != this.config.get("itemsPerPage");
	visualizator(
		this.prepareItemsFlatList(this.setCollapseFlag(roots)),
		$(this.dom.get("body"))
	);
	this.liveUpdatesSubscribe();
};

Echo.StreamClient.prototype.checkTimeframeSatisfy = function() {
	var timeframe = this.config.get("timeframe");
	var self = this;
	var updates = $.foldl([], this.threads, function(thread, acc){
			var ts = thread.timestamp;
			var s = $.foldl(true, timeframe, function(p,a){
					return a ? p(ts) : false;
				});
			if (!s) acc.push(thread);
		});
	$.each(updates, function(i, item) {
		self.applySpotUpdates("delete", item);
	});
};

Echo.StreamClient.prototype.handleLiveUpdatesResponse = function(data) {
	var self = this;
	data = data || {};
	if (data.result == 'error') {
		this.liveUpdatesUnsubscribe();
		return;
	}
	this.nextSince = data.nextSince || 0;
	this.refreshItemsDate();
	this.checkTimeframeSatisfy();
	this.applyLiveUpdates(data.entries);
	if (this.animation.paused) {
		this.rerender("state");
	} else if (!this.animation.initiated) {
		this.animateNextItem();
	}
	this.liveUpdatesSubscribe();
};

Echo.StreamClient.prototype.applyLiveUpdates = function(entries) {
	var self = this;
	$.each(entries || [], function(i, entry) {
		entry = self.normalizeEntry(entry);
		var item = self.items[self.getUniqueID(entry)];
		var action = self.classifyAction(entry);
		if (!item && action != "post") return;
		switch (action) {
			case "post":
				if (item) {
					item.unblock(); // item may be blocked
							// by the moderation action
					self.applySpotUpdates("replace", self.updateItem(entry));
				} else {
					item = self.initItem(entry, true);
					if ((self.isRootItem(item) && self.withinVisibleFrame(item))
							|| self.hasParentItem(item)) {
						self.publish("Stream.Item.onReceive",
							self.prepareBroadcastParams({
								"item": {"data": item.data}
							}));
						self.applySpotUpdates("add", item);
					} else {
						delete self.items[item.unique];
					}
				}
				break;
			case "delete":
				self.applySpotUpdates("delete", item);
				break;
		}
	});
	this.recalcEffectsTimeouts();
};

Echo.StreamClient.prototype.recalcEffectsTimeouts = function() {
	// recalculating timeouts based on amount of items in animation queue
	var s = this;
	var maxTimeouts = {
		"fade": s.config.get("fadeTimeout"),
		"slide": s.config.get("slideTimeout")
	};
	s.timeouts = s.timeouts || {
		"fade": maxTimeouts.fade,
		"slide": maxTimeouts.slide
	};
	if (maxTimeouts.fade == 0 && maxTimeouts.slide == 0) return;
	s.timeouts.coeff = s.timeouts.coeff || {
		"fade": s.timeouts.fade / (maxTimeouts.fade + maxTimeouts.slide),
		"slide": s.timeouts.slide / (maxTimeouts.fade + maxTimeouts.slide)
	};
	var calc = function(timeout, value) {
		value = Math.round(value * s.timeouts.coeff[timeout]);
		if (value < 100) return 0; // no animation for small timeouts
		if (value > maxTimeouts[timeout]) return maxTimeouts[timeout];
		return value;
	};
	// reserving 80% of time between live updates for animation
	var frame = s.config.get("liveUpdatesTimeout") * 1000 * 0.8;
	var msPerItem = s.animation.queue.length ? frame / s.animation.queue.length : frame;
	s.timeouts.fade = calc("fade", msPerItem);
	s.timeouts.slide = calc("slide", msPerItem);
};

Echo.StreamClient.prototype.refreshItemsDate = function() {
	this.traverse(this.threads, function(item) {
		item.calcAge();
		item.rerender("date");
	});
};

Echo.StreamClient.prototype.animateNextItem = function() {
	if (!this.animation.queue.length || this.animation.paused) {
		this.animation.initiated = false;
		return;
	}
	this.animation.initiated = true;
	this.animation.queue.shift().handler();
};

Echo.StreamClient.prototype.applySpotUpdates = function(action, item) {
	var self = this;
	$.each(self.getVisualUpdatesDiff(action, item) || {}, function(k, chunk) {
		var node = self.items[chunk[1]];
		switch (chunk[0]) {
			case "add":
				self.placeItem(node);
				if (node.dom) {
					$(node.dom.content).hide();
					self.animation.queue.push({
						"affectCounter": true,
						"handler": function() {
							self.addItemSpotUpdate(node);
							self.actualizeExpandMarker(item);
						}
					});
				} else self.actualizeExpandMarker(item);
				break;
			case "delete":
				if (node && node.dom) {
					self.animation.queue.push({
						"affectCounter": false,
						"handler": function() {
							self.deleteItemSpotUpdate(node);
							self.actualizeExpandMarker(item);
						}
					});
				} else self.actualizeExpandMarker(item);
				break;
			case "replace":
				if (node && node.dom) {
					node.rerender();
				}
				break;
			case "move":
				if (node && node.dom) {
					self.animation.queue.push({
						"affectCounter": false,
						"handler": function() {
							self.moveItemSpotUpdate(node);
						}
					});
				}
				break;
		}
	});
};

Echo.StreamClient.prototype.addItemSpotUpdate = function(item) {
	var self = this;
	var container = $(item.dom.content);
	if (!this.isRootItem(item)) {
		var parent = this.getParentItem(item);
		if (parent.children.length >= 1 && parent.children[0].id == item.id) {
			// we should re-render item parent when
			// item is the first child for the parent
			this.applySpotUpdates("replace", parent);
			if (self.isReplyFormActive()) {
				// appending reply form
				var root = self.findRootItem(item);
				if (!root.replyForm) {
					root.replyForm = self.initReplyForm(root);
					container.after(root.replyForm.render());
				} else {
					root.replyForm.show("compact");
				}
			}
		}
	}
	var originalBGColor = this.getVisibleColor(container);
	if (this.timeouts.slide) {
		container
		// defining a fixed height for an element
		// to make sliding down process work smoothly
		.css("height", container.css("height"))
		.hide()
		.slideDown(this.timeouts.slide);
	} else {
		container.show();
	}
	var publish = function() {
		self.publish("Stream.Item.onRender", self.prepareBroadcastParams({
			"item": {
				"data": item.data,
				"target": item.dom.content
			}
		}));
	};
	if (this.timeouts.fade) {
		container
		.css({"backgroundColor": this.config.get("flashColor")})
		// Fading out
		.animate(
			{"backgroundColor": originalBGColor},
			this.timeouts.fade,
			"easeInOutExpo",
			function() {
				container.css("backgroundColor", "");
				publish();
				self.animateNextItem();
			}
		);
	} else {
		publish();
		this.animateNextItem();
	}
};

Echo.StreamClient.prototype.deleteItemSpotUpdate = function(item) {
	var self = this;
	var container = $(item.dom.content);
	var remove = function() {
		container.remove();
		delete item.dom;
		delete self.items[item.unique];
		var parent = self.getParentItem(item);
		if (parent && parent.children.length == 0) {
			if (self.isReplyFormActive() &&
				parent.replyForm && !parent.replyForm.text()) {
					parent.replyForm.hide();
			}
			// we should re-render item parent when
			// the last child was deleted
			self.applySpotUpdates("replace", parent);
		}
		if (self.isRootItem(item) && item.replyForm) {
			self.unsubscribe("document.onclick", item.replyForm.closeHandler);
			item.replyForm.remove();
		}
		var itemsCount = $.foldl(0, self.items, function(_item, acc) {
			return acc + 1;
		});
		if (!itemsCount) {
			self.showMessage({
				"type": "empty",
				"message": Echo.Localization.getLabel("emptyStream")
			}, self.dom.get('body'));
		}
		self.animateNextItem();
	};
	if (this.timeouts.slide) {
		container.slideUp(this.timeouts.slide, remove);
	} else {
		remove();
	}
};

Echo.StreamClient.prototype.moveItemSpotUpdate = function(item) {
	$(item.dom.content).remove();
	this.placeItem(item);
	var anchor = $(item.dom.content);
	// move item along with it's children
	this.traverse(item.children || [], function(child) {
		if (child.dom) {
			var dom = $(child.dom.content).remove();
			anchor.after(dom);
			anchor = dom;
		}
	});
	this.moveServiceItems(item);
	this.animateNextItem()
};

Echo.StreamClient.prototype.isRootItem = function(item) {
	return !this.config.get("childrenMaxDepth") || item.id == item.conversation;
};

Echo.StreamClient.prototype.hasParentItem = function(item) {
	return !!this.getParentItem(item);
};

Echo.StreamClient.prototype.maybeMoveItem = function(item) {
	return this.isRootItem(item) && this.config.get("sortOrder").match(/replies|likes/);
};

Echo.StreamClient.prototype.withinVisibleFrame = function(item) {
	if (this.isViewComplete || this.pageAfter == undefined) return true;
	return this.compareItems(this.pageAfter, item, this.config.get("sortOrder"));
};

Echo.StreamClient.prototype.getParentItem = function(item) {
	return this.items[this.getParentUniqueID(item)];
};

Echo.StreamClient.prototype.findRootItem = function(entry) {
	var item = entry;
	while (!this.isRootItem(item)) {
		item = this.getParentItem(item);
	}
	return item;
};

Echo.StreamClient.prototype.compareItems = function(a, b, sort) {
	var self = this;
	switch (sort) {
		case "chronological":
			return a.timestamp > b.timestamp;
		case "reverseChronological":
			return a.timestamp <= b.timestamp;
		case "likesDescending":
		case "repliesDescending":
			var getCount = function(entry) {
				return self.getRespectiveAccumulator(entry, sort);
			};
			return (getCount(a) < getCount(b) ||
					(getCount(a) == getCount(b) &&
						this.compareItems(a, b, "reverseChronological")));
	};
};


Echo.StreamClient.prototype.placeItem = function(item) {
	var self = this;
	var content = item.render();
	var items = this.isRootItem(item)
		? this.threads
		: this.getParentItem(item).children;
	if (items.length > 1) {
		var anchor = this.findAnchor(item, items);
		if (anchor){
			$(anchor[1].dom.content)[anchor[0]](content);
		}
	} else {
		if (this.isRootItem(item)) {
			$(this.dom.get("body")).empty().append(content);
		} else {
			var parent = this.getParentItem(item);
			if (this.isRootItem(parent) && this.config.get("truncateReplies.after") == 0) {
				item.dom = undefined;
				var root = this.findRootItem(item);
				root.whirlpool = this.initWhirlpool(root, [item]);
				$(root.dom.content).after(root.whirlpool.render());
			} else {
				$(parent.dom.content).after(content);
			}
		}
	}
};

Echo.StreamClient.prototype.initReplyForm = function(root, mode) {
	var self = this;
	var config = {
		"data":  {"unique": root.unique},
		"mode": mode || "compact",
		"appkey": self.config.get("appkey"),
		"identityManagerLogin" : self.config.get("identityManagerLogin"),
		"identityManagerSignup" : self.config.get("identityManagerSignup"),
		"identityManagerEdit" : self.config.get("identityManagerEdit"),
		"contextId": self.contextId,
		"targetURL": root.id
	};
	$.each({
		"replyActionString": "actionString",
		"replySubmitPermissions": "submitPermissions"
	}, function(streamKey, submitKey) {
		if (self.config.get(streamKey)) {
			config[submitKey] = self.config.get(streamKey);
		}
	});
	var item = new Echo.ReplyForm(config);
	item.closeHandler = this.subscribe("document.onclick", function(event, data) {
		if (!item.visible || item.text()) return;
		if (root.children.length) {
			if (item.form.config.get("mode") != "compact") {
				item.form.config.set("mode", "compact");
				item.form.rerender();
			} else return;
		} else {
			item.hide();
			self.applySpotUpdates("replace", root);
		}
		self.publish("Stream.Plugins.Reply.onFormCollapse", self.prepareBroadcastParams({
			"form": item.form,
			"item": {
				"data": root.data,
				"target": root.dom.content
			}
		}));
	});
	return item;
};

Echo.StreamClient.prototype.initWhirlpool = function(root, collapsedItems) {
	return new Echo.Whirlpool({
		"data": {"items": collapsedItems},
		"config": new Echo.Config({"truncateReplies": this.config.get("truncateReplies")}),
		"onexpand": function(items) {
			$.each(items, function(i, item) {
				item.collapse = false;
				$(root.whirlpool.dom.content).before(item.render());
			});
			$(root.whirlpool.dom.content).remove();
			delete root.whirlpool;
		}
	});
};

Echo.StreamClient.prototype.findAnchor = function(item, items) {
	var id = this.getItemListIndex(item, items);
	var prev = id - 1, next = id + 1;
	if (items[next] && items[next].dom) return ["before", items[next]];
	if (items[prev]) {
		var last = items[prev];
		while (last.children.length > 0) {
			last = last.children[last.children.length - 1];
		}
		if (last.dom) return ["after", last];
		// no items under expand marker
		if (this.isRootItem(item) && items[prev].whirlpool &&
			this.config.get("truncateReplies.after") < 2) {
				return ["after", items[prev].whirlpool];
		}
	}
	return false;
};

Echo.StreamClient.prototype.getItemListIndex = function(item, items) {
	var id;
	$.each(items || [], function(i, entry) {
		if (entry == item) {
			id = i;
			return false;
		}
	});
	return id;
};

Echo.StreamClient.prototype.getUniqueID = function(entry) {
	return entry.unique || entry.object.id + entry.target.conversationID;
}

Echo.StreamClient.prototype.getParentUniqueID = function(entry) {
	return entry.parentUnique || entry.target.id + entry.target.conversationID;
}

Echo.StreamClient.prototype.initItem = function(entry, isLive) {
	var self = this;
	var content_type = entry.object.content_type;
	var item = new Echo.Item({
		"children": [],
		"collapse": false,
		"config": new Echo.Config({
			"aggressiveSanitization": this.config.get("aggressiveSanitization"),
			"communityFlag": Echo.User.logged() && this.config.get("communityFlag"),
			"contentTransformations": this.config.get("contentTransformations." +
									content_type, {}),
			"defaultProviderIcon": this.config.get("providerIcon"),
			"like" : this.config.get("like"),
			"limits": {
				"body": this.config.get("maxBodyCharacters"),
				"reLink": this.config.get("maxReLinkLength"),
				"reTitle": this.config.get("maxReTitleLength"),
				"bodyLink": this.config.get("maxBodyLinkLength"),
				"tags": this.config.get("maxTagLength"),
				"markers": this.config.get("maxMarkerLength")
			},
			"openLinksInNewWindow": this.config.get("openLinksInNewWindow"),
			"optimizedContext": this.config.get("optimizedContext"),
			"replaceNewlines": this.config.get("replaceNewlines"),
			"reply": this.isReplyFormActive(),
			"reTag": this.config.get("reTag"),
			"viaLabel": this.config.get("viaLabel")
		}),
		"contextId": this.contextId,
		"conversation": entry.target.conversationID, // short cut for "conversationID" field
		"data": entry,
		"depth": 0,
		"id": entry.object.id, // short cut for "id" item field
		"live": isLive,
		"parentUnique": this.getParentUniqueID(entry),
		"threading": true,
		"timestamp": this.timestampFromW3CDTF(entry.object.published),
		"unique": this.getUniqueID(entry)
	});
	this.items[item.unique] = item;
	return item;
};

Echo.StreamClient.prototype.updateItem = function(entry) {
	var item = this.items[this.getUniqueID(entry)];
	item.data = entry;
	return item;
};

Echo.StreamClient.prototype.addItemToList = function(items, item, sort) {
	var self = this;
	if (item.live || item.forceInject) {
		var inserted = false;
		$.each(items || [], function(i, entry) {
			if (self.compareItems(entry, item, sort)) {
				items.splice(i, 0, item);
				inserted = true;
				return false;
			}
		});
		if (!inserted) {
			items.push(item);
		}
		delete item.forceInject;
	} else {
		items.push(item);
	}
};

Echo.StreamClient.prototype.applyStructureUpdates = function(action, item) {
	switch (action) {
		case "add":
			if (!this.isRootItem(item)) {
				var parent = this.getParentItem(item);
				// avoiding problem with missing parent
				if (!parent) {
					delete this.items[item.unique];
					return;
				}
				item.depth = parent.depth + 1;
				if (item.depth > this.config.get("childrenMaxDepth")) {
					item.depth = this.config.get("childrenMaxDepth");
					item.threading = false;
					// replace parent of the item
					item.parentUnique = parent.parentUnique;
					item.data.target.id = parent.data.target.id;
					item.forceInject = true;
					this.items[item.unique] = item;
					this.applyStructureUpdates("add", item);
					return;
				}
				this.addItemToList(parent.children, item, this.config.get("children.sortOrder"));
				if (item.live) this.setCollapseFlag([this.findRootItem(item)]);
			} else {
				this.addItemToList(this.threads, item, this.config.get("sortOrder"));
			}
			break;
		case "delete":
			var container = this.isRootItem(item)
				? this.threads
				: this.items[item.parentUnique].children;
			container.splice(this.getItemListIndex(item, container), 1);
			break;
		case "replace":
			if (this.maybeMoveItem(item)) {
				// item may change its position during "replace" operation
				// if sortOrder is defined as
				// "repliesDescending" or "likesDescending"
				this.applyStructureUpdates("delete", item);
				item.forceInject = true;
				this.applyStructureUpdates("add", item);
			}
			break;
	};
};

Echo.StreamClient.prototype.normalizeEntry = function(entry) {
	if (entry.normalized) return entry;
	var self = this;
	entry.normalized = true;
	// detecting actual target
	$.each(entry.targets || [], function(i, target) {
		if ((target.id == target.conversationID) ||
			(target.id == entry.object.id) ||
			(self.items[target.id + target.conversationID])) {
				entry.target = target;
		}
	});
	entry.object.content_type = entry.object.content_type || "text";
	entry.object.accumulators = entry.object.accumulators || {};
	entry.object.accumulators.repliesCount =
				parseInt(entry.object.accumulators.repliesCount || "0");
	entry.object.accumulators.likesCount =
				parseInt(entry.object.accumulators.likesCount || "0");
	entry.object.context = entry.object.context || [];
	entry.object.flags = entry.object.flags || [];
	entry.object.likes = entry.object.likes || [];
	entry.target = entry.target || entry.targets[0] || {};
	entry.target.conversationID = entry.target.conversationID || entry.object.id;
	entry.actor.title = entry.actor.title || Echo.Localization.getLabel('guest');
	entry.source = entry.source || {};
	entry.provider = entry.provider || {};
	return entry;
};

Echo.StreamClient.prototype.getVisualUpdatesDiff = function(action, item) {
	var self = this, diff = {};
	var root = this.findRootItem(item);
	var normalize = function() {
		// "add" actions should go first
		var result = [];
		$.each(diff, function(id, act) {
			result.push([act, id]);
		});
		return result.sort(function(a, b) {
			return (a[0] == "add" && b[0] != "add") ? -1 : 1;
		});
	};
	if (action == "delete" && item.children) {
		// clean up item's children
		self.traverse(item.children, function(node) {
			if (!node.collapse) diff[node.unique] = "delete";
		});
	}
	if (!root.whirlpool || action == "replace" || root.unique == item.unique) {
		if (action == "replace" && this.maybeMoveItem(item)) {
			var oldIdx = this.getItemListIndex(item, this.threads);
			this.applyStructureUpdates(action, item);
			var newIdx = this.getItemListIndex(item, this.threads);
			if (oldIdx != newIdx) {
				action = "move";
			}
		} else {
			this.applyStructureUpdates(action, item);
		}
		diff[item.unique] = action;
		return normalize();
	}
	var takeSnapshot = function() {
		return self.traverse(root.children, function(node, acc) {
			acc[node.unique] = node.collapse ? "delete" : "add";
			return acc;
		}, {});
	};
	var obsolete = takeSnapshot();
	this.applyStructureUpdates(action, item);
	var actual = takeSnapshot();
	$.each(obsolete, function(id, state) {
		if (!actual[id]) {
			diff[id] = "delete";
		} else if (actual[id] != obsolete[id]) {
			diff[id] = actual[id];
		}
	});
	$.each(actual, function(id, state) {
		if (!diff[id] && !obsolete[id]) {
			diff[id] = actual[id];
		}
	});
	return normalize();
};

Echo.StreamClient.prototype.getVisibleColor = function(elem) {
	// calculate visible color of element (transparent is not visible)
	var color;
	do {
		color = elem.css('backgroundColor');
		if (color != '' && color != 'transparent' || $.nodeName(elem.get(0), 'body')) {
			break;
		}
	} while (elem = elem.parent());
	return color;
};

Echo.StreamClient.prototype.addCss = function() {
	var self = this;
	$.addCss(
		'.echo-stream-message-wrapper { padding: 15px 0px; text-align: center; -moz-border-radius: 0.5em; -webkit-border-radius: 0.5em; border: 1px solid #E4E4E4; }' +
		'.echo-stream-message-empty, .echo-stream-message-loading, .echo-stream-message-error { display: inline-block; height: 16px; padding-left: 21px; background: no-repeat left center; }' +
		'.echo-stream-message-empty { background-image: url(//cdn.js-kit.com/images/information.png); }' +
		'.echo-stream-message-loading { background-image: url(//cdn.js-kit.com/images/loading.gif); }' +
		'.echo-stream-message-error { background-image: url(//cdn.js-kit.com/images/warning.gif); }' +
		'.echo-stream-header { margin: 10px 0px 10px 20px; }' +
		'.echo-stream-state { float: right; }' +
		'.echo-stream-state-picture { display: inline-block; height: 9px; width: 8px; }' +
		'.echo-stream-state-picture-paused { background: url("//cdn.js-kit.com/images/control_pause.png") no-repeat center center; }' +
		'.echo-stream-state-picture-live { background: url("//cdn.js-kit.com/images/control_play.png") no-repeat center center; }' +
		'.echo-stream-state-message { margin-left: 5px;}' +
		'.echo-stream-curate { float: right; margin-left: 15px; cursor: pointer; font-family: Arial; font-size: 11px; }' +
		'.echo-stream-brand { text-align: right; display: none; }' +
		'.echo-stream-brand-message { display: inline-block; height: 17px; line-height: 17px; border: none; padding-right: 48px; background: url(//cdn.js-kit.com/images/echo-brand.png) no-repeat right; font-size: 10px; font-family: Arial; }' +
		'.echo-stream-container a.echo-stream-brand-link { text-decoration: none; color: #666666; } ' +
		'.echo-stream-more-hover { background-color: #E4E4E4; }' +
		'.echo-stream-more-wrapper { text-align: center; border: solid 1px #E4E4E4; margin-top: 10px; padding: 10px; -moz-border-radius: 0.5em; -webkit-border-radius: 0.5em; cursor: pointer; font-weight: bold; }' +
		'.echo-edit-item-container .echo-submit-container { margin: 10px; }' +
		'.echo-curation-tabs-queries span { background: no-repeat center left url("//cdn.js-kit.com/images/curation/tabs/queries.png"); }' +
		'.echo-curation-tabs-actions span { background: no-repeat center left url("//cdn.js-kit.com/images/curation/tabs/actions.png"); }'
	, 'stream');

	$.addCss(
		'.echo-whirlpool-container { text-align: center; }' +
		'.echo-whirlpool-container-collapse-all { text-align: left; }' +
		'.echo-whirlpool-message { display: inline-block; padding-left: 18px; background: url("//cdn.js-kit.com/images/whirlpool.png") no-repeat center left; }'
	, 'whirlpool');

	$.addCss(
		'.echo-item-container-root { padding: 10px 0px; }' +
		'.echo-item-container-root-thread { padding: 10px 0px 0px 0px; }' +
		'.echo-item-container-child { padding: 10px; margin: 0px 20px 2px 0px; }' +
		'.echo-item-container-child-thread { padding: 10px; margin: 0px 20px 2px 0px; }' +
		'.echo-item-wrapper-root { margin-left: 58px; }' +
		'.echo-item-wrapper-child { margin-left: 34px; }' +
		'.echo-item-markers { line-height: 16px; background: url(//cdn.js-kit.com/images/curation/metadata/marker.png) no-repeat; padding: 0px 0px 4px 21px; margin-top: 7px; }' +
		'.echo-item-tags { line-height: 16px; background: url(//cdn.js-kit.com/images/tag_blue.png) no-repeat; padding: 0px 0px 4px 21px; }' +
		'.echo-item-likes { line-height: 16px; background: url(//cdn.js-kit.com/images/likes.png) no-repeat 0px 4px; padding: 0px 0px 4px 21px; }' +
		'.echo-item-metadata { display: none; }' +
		'.echo-item-metadata-title { font-weight: bold; line-height: 25px; height: 25px; margin-right: 5px; }' +
		'.echo-item-metadata-icon { display: inline-block; padding-left: 26px; }' +
		'div.echo-item-metadata-userIP { border-top: 1px solid #e1e1e1; border-bottom: 1px solid #e1e1e1; margin-top: 10px; display: none; }' +
		'div.echo-item-metadata-userID { border-bottom: 1px solid #e1e1e1; border-top: 1px solid #e1e1e1;}' +
		'span.echo-item-metadata-userIP { background: url("//cdn.js-kit.com/images/curation/metadata/computer.png") no-repeat left center; }' +
		'span.echo-item-metadata-userID { background: url("//cdn.js-kit.com/images/curation/metadata/user.png") no-repeat left center; }' +
		'.echo-item-modeSwitch { display:none; float: right; width: 16px; height: 16px; background:url("//cdn.js-kit.com/images/curation/metadata/flip.png") no-repeat; }' +
		'.echo-item-childrenMarker { border-color: transparent transparent #ECEFF5; border-width: 0px 11px 11px; border-style: solid; margin: 3px 0px 0px 77px; height: 1px; width: 0px; display: none; }' + // This is magic "arrow up". Only color and margins could be changed
		'.echo-item-container-root-thread .echo-item-childrenMarker { display: block; }' +
		'.echo-item-avatar-wrapper { float: left; padding: 0px 10px 0px 0px; }' +
		'.echo-item-status { width: 48px; height: 24px; }' +
		'.echo-item-status-child { width: 24px; height: 48px; }' +
		'.echo-item-status-checkbox { float: left; margin: 4px; }' +
		'.echo-item-status-child .echo-item-status-checkbox { display: block; }' +
		'.echo-item-status-icon { float: right; margin: 4px; width: 16px; height: 16px; }' +
		// statuses
		'.echo-item-status-Untouched { background: #00aaff; }' +
		'.echo-item-status-ModeratorApproved { background: #bdfb6d; }' +
		'.echo-item-status-ModeratorDeleted { background: #f20202; }' +
		'.echo-item-status-SystemFlagged, .echo-item-status-CommunityFlagged, .echo-item-status-ModeratorFlagged { background: #ff9e00; }' +
		// status icons
		$.map(self.states, function(name){
			return self.substitute('.echo-item-status-icon-{Data:name} { background: url("{Data:img}") no-repeat; }', {"name": name, "img": "//cdn.js-kit.com/images/curation/status/" + name.toLowerCase() + ".png"});
		}).join("\n") +
		'.echo-item-authorName { font-size: 15px; font-family: Arial, sans-serif; font-weight: bold; }' +
		'.echo-item-re { font-weight: bold; }' +
		'.echo-item-re a:link, a:visited, a:active { text-decoration: none; }' +
		'.echo-item-re a:hover { text-decoration: underline; }' +
		'.echo-item-body { padding-top: 4px; }' +
		'.echo-item-control { cursor: pointer; }' +
		'.echo-item-controls { float: left; margin-left: 3px; }' +
		'.echo-item-sourceIcon { float: left; height: 16px; padding-left: 21px; background: no-repeat; }' +
		'.echo-item-date, .echo-item-from, .echo-item-via { float: left; }' +
		'.echo-item-from a, .echo-item-via a { text-decoration: none; color: #C6C6C6; }' +
		'.echo-item-from a:hover, .echo-item-via a:hover { color: #476CB8; }' +
		'.echo-item-tag { display: inline-block; height: 16px; background: url("//cdn.js-kit.com/images/tag_blue.png") no-repeat; padding-left: 18px; }' +
		'.echo-item-blocker-backdrop { position: absolute; background: #FFFFFF; opacity: 0.7; z-index: 100; }' +
		'.echo-item-blocker-message { position: absolute; z-index: 200; width: 200px; height: 20px; text-align: center; padding: 4px 0px 0px; background-color: #FFFF99; border: 1px solid #C6C677; opacity: 0.7; -moz-border-radius: 0.5em 0.5em 0.5em 0.5em; }'
	, 'item');

	var itemDepthRules = [];
	for (var i = 0; i <= this.config.get("childrenMaxDepth"); i++) {
		itemDepthRules.push('.echo-item-depth-' + i + ' { margin-left: ' + (i ? 68 + (i - 1) * 44 : 0) + 'px; }');
	}
	$.addCss(itemDepthRules.join('\n'), 'item-depths');

	if ($.browser.msie) {
		$.addCss(
			'.echo-item-childrenMarker { font-size: 1px; line-height: 1px; filter: chroma(color=black); }' + // filter:chroma needs to avoid transparent borders as black in ie6
			'.echo-item-status-checkbox { margin: 1px; }' +
			'.echo-item-blocker-backdrop, .echo-item-blocker-message { filter:Alpha(Opacity=70); }' +
			'.echo-stream-container { zoom: 1; }' +
			'.echo-item-content { zoom: 1; }' +
			'.echo-item-container { zoom: 1; }' +
			'.echo-item-sub-wrapper { zoom: 1; }'
		, 'ie');
	}
};

Echo.StreamClient.prototype.setCollapseFlag = function(thread) {
	var self = this, limits = this.config.get("truncateReplies");
	if (limits.after < 0) return thread;
	$.each(thread || [], function(i, item) {
		var count = self.traverse(item.children, function(c, acc) { return ++acc; }, 0);
		self.traverse(item.children, function(child, index) {
			child.collapse =
				count > limits.after &&
				index > limits.recent &&
				index + limits.legacy <= count;
			return ++index;
		}, 1);
	});
	return thread;
};

Echo.StreamClient.prototype.traverse = function(tree, callback, acc) {
	var self = this;
	$.each(tree || [], function(i, item) {
		acc = self.traverse(item.children, callback, callback(item, acc));
	});
	return acc;
};

Echo.StreamClient.prototype.prepareItemsFlatList = function(threads) {
	var self = this;
	return $.foldl([], threads, function(thread, acc) {
		delete thread.whirlpool;
		var particles = self.traverse(thread.children, function(child, _acc) {
			_acc[child.collapse ? "collapsed" : "visible"].push(child);
			return _acc;
		}, {"visible": [thread], "collapsed": []});
		if (particles.collapsed.length > 0) {
			thread.whirlpool = self.initWhirlpool(thread, particles.collapsed);
			// take into account that root node is always in the list 
			var anchorIndex = self.config.get("truncateReplies.recent") + 1;
			particles.visible.splice(anchorIndex, 0, thread.whirlpool);
		}
		if (self.isReplyFormActive() && thread.children.length) {
			if (!thread.replyForm) {
				thread.replyForm = self.initReplyForm(thread);
			}
			particles.visible.push(thread.replyForm);
		}
		return acc.concat(particles.visible);
	});
};

Echo.StreamClient.prototype.classifyAction = function(entry) {
	return (entry.verbs[0] == "http://activitystrea.ms/schema/1.0/delete") ? "delete" : "post";
};

Echo.StreamClient.prototype.liveUpdatesRequest = function() {
	var self = this;
	this.sendRequest({
		"endpoint": "search",
		"query": {
			"appkey": self.config.get("appkey"),
			"q": self.constructSearchQuery(),
			"since": self.nextSince || 0
		}
	}, function(data) {
		self.handleLiveUpdatesResponse(data);
	});
};

Echo.StreamClient.prototype.liveUpdatesUnsubscribe = function() {
	if (this.liveUpdatesTimer) {
		clearTimeout(this.liveUpdatesTimer);
	}
};

Echo.StreamClient.prototype.liveUpdatesSubscribe = function() {
	var self = this;
	if (!this.config.get("liveUpdates")) return;
	this.liveUpdatesUnsubscribe();
	this.liveUpdatesTimer = setTimeout(function() {
		self.liveUpdatesRequest();
	}, this.config.get("liveUpdatesTimeout") * 1000);
};

Echo.StreamClient.prototype.moveServiceItems = function(item) {
	var parent = this.findRootItem(item);
	if (!parent.whirlpool && !parent.replyForm) return;
	if (parent.whirlpool) {
		$(parent.whirlpool.dom.content).remove();
	}
	var anchor;
	var items = this.prepareItemsFlatList([parent]);
	if (parent.whirlpool) {
		var idx = this.getItemListIndex(parent.whirlpool, items);
		if (idx) {
			anchor = items[idx - 1].dom.content;
			$(anchor).after(parent.whirlpool.render());
		}
	}
	if (parent.replyForm) {
		var idx = this.getItemListIndex(parent.replyForm, items);
		anchor = idx ? items[idx - 1].dom.content : item.dom.content;
		$(anchor).after($(parent.replyForm.dom.content));
	}
};

Echo.StreamClient.prototype.actualizeExpandMarker = function(item) {
	if (this.isRootItem(item)) return;
	var parent = this.findRootItem(item);
	if (parent.whirlpool) {
		var content = parent.whirlpool.dom.content;
		this.prepareItemsFlatList([parent]);
		if (!parent.whirlpool) {
			$(content).remove();
		} else {
			$(content).replaceWith(parent.whirlpool.render());
			if (parent.whirlpool.data.count == 1) {
				if (this.timeouts.slide) {
					$(parent.whirlpool.dom.content)
						.hide()
						.slideDown(this.timeouts.slide);
				}
			}
		}
	}
};

})(jQuery);

