text: // Frappe Chat
  // Author - Achilles Rasquinha <achilles@frappe.io>
  
  import Fuse   from 'fuse.js'
  import hyper  from '../lib/hyper.min'
  
  import './socketio_client'
  
  import './ui/dialog'
  import './ui/capture'
  
  import './utils/user'
  
  /* eslint semi: "never" */
  // Fuck semicolons - https://mislav.net/2010/05/semicolons
  
  // frappe extensions
  
  /**
   * @description The base class for all Frappe Errors.
   *
   * @example
   * try
   *      throw new frappe.Error("foobar")
   * catch (e)
   *      console.log(e.name)
   * // returns "FrappeError"
   *
   * @see  https://stackoverflow.com/a/32749533
   * @todo Requires "transform-builtin-extend" for Babel 6
   */
  frappe.Error = Error
  // class extends Error {
  // 	constructor (message) {
  // 		super (message)
  
  // 		this.name = 'FrappeError'
  
  // 		if ( typeof Error.captureStackTrace === 'function' )
  // 			Error.captureStackTrace(this, this.constructor)
  // 		else
  // 			this.stack = (new Error(message)).stack
  // 	}
  // }
  
  /**
   * @description TypeError
   */
  frappe.TypeError  = TypeError
  // class extends frappe.Error {
  // 	constructor (message) {
  // 		super (message)
  
  // 		this.name = this.constructor.name
  // 	}
  // }
  
  /**
   * @description ValueError
   */
  frappe.ValueError = Error
  // class extends frappe.Error {
  // 	constructor (message) {
  // 		super (message)
  
  // 		this.name = this.constructor.name
  // 	}
  // }
  
  /**
   * @description ImportError
   */
  frappe.ImportError = Error
  // class extends frappe.Error {
  // 	constructor (message) {
  // 		super (message)
  
  // 		this.name  = this.constructor.name
  // 	}
  // }
  
  // frappe.datetime
  frappe.provide('frappe.datetime')
  
  /**
   * @description Frappe's datetime object. (Inspired by Python's datetime object).
   *
   * @example
   * const datetime = new frappe.datetime.datetime()
   */
  frappe.datetime.datetime = class {
  	/**
  	 * @description Frappe's datetime Class's constructor.
  	 */
  	constructor (instance, format = null) {
  		if ( typeof moment === 'undefined' )
  			throw new frappe.ImportError(`Moment.js not installed.`)
  
  		this.moment = instance ? moment(instance, format) : moment()
  	}
  
  	/**
  	 * @description Returns a formatted string of the datetime object.
  	 */
  	format (format = null) {
  		const  formatted = this.moment.format(format)
  		return formatted
  	}
  }
  
  /**
   * @description Frappe's daterange object.
   *
   * @example
   * const range = new frappe.datetime.range(frappe.datetime.now(), frappe.datetime.now())
   * range.contains(frappe.datetime.now())
   */
  frappe.datetime.range   = class {
  	constructor (start, end) {
  		if ( typeof moment === undefined )
  			throw new frappe.ImportError(`Moment.js not installed.`)
  
  		this.start = start
  		this.end   = end
  	}
  
  	contains (datetime) {
  		const  contains = datetime.moment.isBetween(this.start.moment, this.end.moment)
  		return contains
  	}
  }
  
  /**
   * @description Returns the current datetime.
   *
   * @example
   * const datetime = new frappe.datetime.now()
   */
  frappe.datetime.now   = () => new frappe.datetime.datetime()
  
  frappe.datetime.equal = (a, b, type) => {
  	a = a.moment
  	b = b.moment
  
  	const equal = a.isSame(b, type)
  
  	return equal
  }
  
  /**
   * @description Compares two frappe.datetime.datetime objects.
   *
   * @param   {frappe.datetime.datetime} a - A frappe.datetime.datetime/moment object.
   * @param   {frappe.datetime.datetime} b - A frappe.datetime.datetime/moment object.
   *
   * @returns {number} 0 (if a and b are equal), 1 (if a is before b), -1 (if a is after b).
   *
   * @example
   * frappe.datetime.compare(frappe.datetime.now(), frappe.datetime.now())
   * // returns 0
   * const then = frappe.datetime.now()
   *
   * frappe.datetime.compare(then, frappe.datetime.now())
   * // returns 1
   */
  frappe.datetime.compare = (a, b) => {
  	a = a.moment
  	b = b.moment
  
  	if ( a.isBefore(b) )
  		return  1
  	else
  	if ( b.isBefore(a) )
  		return -1
  	else
  		return  0
  }
  
  // frappe.quick_edit
  frappe.quick_edit      = (doctype, docname, fn) => {
  	return new Promise(resolve => {
  		frappe.model.with_doctype(doctype, () => {
  			frappe.db.get_doc(doctype, docname).then(doc  => {
  				const meta     = frappe.get_meta(doctype)
  				const fields   = meta.fields
  				const required = fields.filter(f => f.reqd || f.bold && !f.read_only)
  
  				required.map(f => {
  					if(f.fieldname == 'content' && doc.type == 'File') {
  						f['read_only'] = 1;
  					}
  				})
  
  				const dialog   = new frappe.ui.Dialog({
  					 title: __(`Edit ${doctype} (${docname})`),
  					fields: required,
  					action: {
  						primary: {
  							   label: __("Save"),
  							onsubmit: (values) => {
  								frappe.call('frappe.client.save',
  									{ doc: { doctype: doctype, docname: docname, ...doc, ...values } })
  									  .then(r => {
  										if ( fn )
  											fn(r.message)
  
  										resolve(r.message)
  									  })
  
  								dialog.hide()
  							}
  						},
  						secondary: {
  							label: __("Discard")
  						}
  					}
  				})
  				dialog.set_values(doc)
  
  				const $element = $(dialog.body)
  				$element.append(`
  					<div class="qe-fp" style="padding-top: '15px'; padding-bottom: '15px'; padding-left: '7px'">
  						<button class="btn btn-default btn-sm">
  							${__("Edit in Full Page")}
  						</button>
  					</div>
  				`)
  				$element.find('.qe-fp').click(() => {
  					dialog.hide()
  					frappe.set_route(`Form/${doctype}/${docname}`)
  				})
  
  				dialog.show()
  			})
  		})
  	})
  }
  
  // frappe._
  // frappe's utility namespace.
  frappe.provide('frappe._')
  
  // String Utilities
  
  /**
   * @description Python-inspired format extension for string objects.
   *
   * @param  {string} string - A string with placeholders.
   * @param  {object} object - An object with placeholder, value pairs.
   *
   * @return {string}        - The formatted string.
   *
   * @example
   * frappe._.format('{foo} {bar}', { bar: 'foo', foo: 'bar' })
   * // returns "bar foo"
   */
  frappe._.format = (string, object) => {
  	for (const key in object)
  		string  = string.replace(`{${key}}`, object[key])
  
  	return string
  }
  
  /**
   * @description Fuzzy Search a given query within a dataset.
   *
   * @param  {string} query   - A query string.
   * @param  {array}  dataset - A dataset to search within, can contain singletons or objects.
   * @param  {object} options - Options as per fuze.js
   *
   * @return {array}          - The fuzzy matched index/object within the dataset.
   *
   * @example
   * frappe._.fuzzy_search("foobar", ["foobar", "bartender"])
   * // returns [0, 1]
   *
   * @see http://fusejs.io
   */
  frappe._.fuzzy_search = (query, dataset, options) => {
  	const DEFAULT     = {
  				shouldSort: true,
  				 threshold: 0.6,
  				  location: 0,
  				  distance: 100,
  		minMatchCharLength: 1,
  		  maxPatternLength: 32
  	}
  	options       = { ...DEFAULT, ...options }
  
  	const fuse    = new Fuse(dataset, options)
  	const result  = fuse.search(query)
  
  	return result
  }
  
  /**
   * @description Pluralizes a given word.
   *
   * @param  {string} word  - The word to be pluralized.
   * @param  {number} count - The count.
   *
   * @return {string}       - The pluralized string.
   *
   * @example
   * frappe._.pluralize('member',  1)
   * // returns "member"
   * frappe._.pluralize('members', 0)
   * // returns "members"
   *
   * @todo Handle more edge cases.
   */
  frappe._.pluralize = (word, count = 0, suffix = 's') => `${word}${count === 1 ? '' : suffix}`
  
  /**
   * @description Captializes a given string.
   *
   * @param   {word}  - The word to be capitalized.
   *
   * @return {string} - The capitalized word.
   *
   * @example
   * frappe._.capitalize('foobar')
   * // returns "Foobar"
   */
  frappe._.capitalize = word => `${word.charAt(0).toUpperCase()}${word.slice(1)}`
  
  // Array Utilities
  
  /**
   * @description Returns the first element of an array.
   *
   * @param   {array} array - The array.
   *
   * @returns - The first element of an array, undefined elsewise.
   *
   * @example
   * frappe._.head([1, 2, 3])
   * // returns 1
   * frappe._.head([])
   * // returns undefined
   */
  frappe._.head = arr => frappe._.is_empty(arr) ? undefined : arr[0]
  
  /**
   * @description Returns a copy of the given array (shallow).
   *
   * @param   {array} array - The array to be copied.
   *
   * @returns {array}       - The copied array.
   *
   * @example
   * frappe._.copy_array(["foobar", "barfoo"])
   * // returns ["foobar", "barfoo"]
   *
   * @todo Add optional deep copy.
   */
  frappe._.copy_array = array => {
  	if ( Array.isArray(array) )
  		return array.slice()
  	else
  		throw frappe.TypeError(`Expected Array, recieved ${typeof array} instead.`)
  }
  
  /**
   * @description Check whether an array|string|object|jQuery is empty.
   *
   * @param   {any}     value - The value to be checked on.
   *
   * @returns {boolean}       - Returns if the object is empty.
   *
   * @example
   * frappe._.is_empty([])      // returns true
   * frappe._.is_empty(["foo"]) // returns false
   *
   * frappe._.is_empty("")      // returns true
   * frappe._.is_empty("foo")   // returns false
   *
   * frappe._.is_empty({ })            // returns true
   * frappe._.is_empty({ foo: "bar" }) // returns false
   *
   * frappe._.is_empty($('.papito'))   // returns false
   *
   * @todo Handle other cases.
   */
  frappe._.is_empty = value => {
  	let empty = false
  
  	if ( value === undefined || value === null )
  		empty = true
  	else
  	if ( Array.isArray(value) || typeof value === 'string' || value instanceof $ )
  		empty = value.length === 0
  	else
  	if ( typeof value === 'object' )
  		empty = Object.keys(value).length === 0
  
  	return empty
  }
  
  /**
   * @description Converts a singleton to an array, if required.
   *
   * @param {object} item - An object
   *
   * @example
   * frappe._.as_array("foo")
   * // returns ["foo"]
   *
   * frappe._.as_array(["foo"])
   * // returns ["foo"]
   *
   * @see https://docs.oracle.com/javase/8/docs/api/java/util/Arrays.html#asList-T...-
   */
  frappe._.as_array = item => Array.isArray(item) ? item : [item]
  
  /**
   * @description Return a singleton if array contains a single element.
   *
   * @param   {array}        list - An array to squash.
   *
   * @returns {array|object}      - Returns an array if there's more than 1 object else the first object itself.
   *
   * @example
   * frappe._.squash(["foo"])
   * // returns "foo"
   *
   * frappe._.squash(["foo", "bar"])
   * // returns ["foo", "bar"]
   */
  frappe._.squash = list => Array.isArray(list) && list.length === 1 ? list[0] : list
  
  /**
   * @description Returns true, if the current device is a mobile device.
   *
   * @example
   * frappe._.is_mobile()
   * // returns true|false
   *
   * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Browser_detection_using_the_user_agent
   */
  frappe._.is_mobile = () => {
  	const regex    = new RegExp("Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini", "i")
  	const agent    = navigator.userAgent
  	const mobile   = regex.test(agent)
  
  	return mobile
  }
  
  /**
   * @description Removes falsey values from an array.
   *
   * @example
   * frappe._.compact([1, 2, false, NaN, ''])
   * // returns [1, 2]
   */
  frappe._.compact   = array => array.filter(Boolean)
  
  // extend utils to base.
  frappe.utils       = { ...frappe.utils, ...frappe._ }
  
  // frappe extensions
  
  // frappe.user extensions
  /**
   * @description Returns the first name of a User.
   *
   * @param {string} user - User
   *
   * @returns The first name of the user.
   *
   * @example
   * frappe.user.first_name("Rahul Malhotra")
   * // returns "Rahul"
   */
  frappe.provide('frappe.user')
  frappe.user.first_name = user => frappe._.head(frappe.user.full_name(user).split(" "))
  
  frappe.provide('frappe.ui.keycode')
  frappe.ui.keycode = { RETURN: 13 }
  
  /**
   * @description Frappe's Store Class
   */
   // frappe.stores  - A registry for frappe stores.
  frappe.provide('frappe.stores')
  frappe.stores = [ ]
  frappe.Store  = class
  {
  	/**
  	 * @description Frappe's Store Class's constructor.
  	 *
  	 * @param {string} name - Name of the logger.
  	 */
  	constructor (name) {
  		if ( typeof name !== 'string' )
  			throw new frappe.TypeError(`Expected string for name, got ${typeof name} instead.`)
  		this.name = name
  	}
  
  	/**
  	 * @description Get instance of frappe.Store (return registered one if declared).
  	 *
  	 * @param {string} name - Name of the store.
  	 */
  	static get (name) {
  		if ( !(name in frappe.stores) )
  			frappe.stores[name] = new frappe.Store(name)
  		return frappe.stores[name]
  	}
  
  	set (key, value) { localStorage.setItem(`${this.name}:${key}`, value) }
  	get (key, value) { return localStorage.getItem(`${this.name}:${key}`) }
  }
  
  // frappe.loggers - A registry for frappe loggers.
  frappe.provide('frappe.loggers')
  /**
   * @description Frappe's Logger Class
   *
   * @example
   * frappe.log       = frappe.Logger.get('foobar')
   * frappe.log.level = frappe.Logger.DEBUG
   *
   * frappe.log.info('foobar')
   * // prints '[timestamp] foobar: foobar'
   */
  frappe.Logger = class {
  	/**
  	 * @description Frappe's Logger Class's constructor.
  	 *
  	 * @param {string} name - Name of the logger.
  	 */
  	constructor (name, level) {
  		if ( typeof name !== 'string' )
  			throw new frappe.TypeError(`Expected string for name, got ${typeof name} instead.`)
  
  		this.name   = name
  		this.level  = level
  
  		if ( !this.level ) {
  			if ( frappe.boot.developer_mode )
  				this.level = frappe.Logger.ERROR
  			else
  				this.level = frappe.Logger.NOTSET
  		}
  		this.format = frappe.Logger.FORMAT
  	}
  
  	/**
  	 * @description Get instance of frappe.Logger (return registered one if declared).
  	 *
  	 * @param {string} name - Name of the logger.
  	 */
  	static get (name, level) {
  		if ( !(name in frappe.loggers) )
  			frappe.loggers[name] = new frappe.Logger(name, level)
  		return frappe.loggers[name]
  	}
  
  	debug (message) { this.log(message, frappe.Logger.DEBUG) }
  	info  (message) { this.log(message, frappe.Logger.INFO)  }
  	warn  (message) { this.log(message, frappe.Logger.WARN)  }
  	error (message) { this.log(message, frappe.Logger.ERROR) }
  
  	log (message, level) {
  		const timestamp   = frappe.datetime.now()
  
  		if ( level.value <= this.level.value ) {
  			const format  = frappe._.format(this.format, {
  				time: timestamp.format('HH:mm:ss'),
  				name: this.name
  			})
  			console.log(`%c ${format}:`, `color: ${level.color}`, message)
  		}
  	}
  }
  
  frappe.Logger.DEBUG  = { value: 10, color: '#616161', name: 'DEBUG'  }
  frappe.Logger.INFO   = { value: 20, color: '#2196F3', name: 'INFO'   }
  frappe.Logger.WARN   = { value: 30, color: '#FFC107', name: 'WARN'   }
  frappe.Logger.ERROR  = { value: 40, color: '#F44336', name: 'ERROR'  }
  frappe.Logger.NOTSET = { value:  0,                   name: 'NOTSET' }
  
  frappe.Logger.FORMAT = '{time} {name}'
  
  // frappe.chat
  frappe.provide('frappe.chat')
  
  frappe.log = frappe.Logger.get('frappe.chat', frappe.Logger.NOTSET)
  
  // frappe.chat.profile
  frappe.provide('frappe.chat.profile')
  
  /**
   * @description Create a Chat Profile.
   *
   * @param   {string|array} fields - (Optional) fields to be retrieved after creating a Chat Profile.
   * @param   {function}     fn     - (Optional) callback with the returned Chat Profile.
   *
   * @returns {Promise}
   *
   * @example
   * frappe.chat.profile.create(console.log)
   *
   * frappe.chat.profile.create("status").then(console.log) // { status: "Online" }
   */
  frappe.chat.profile.create = (fields, fn) => {
  	if ( typeof fields === "function" ) {
  		fn     = fields
  		fields = null
  	} else
  	if ( typeof fields === "string" )
  		fields = frappe._.as_array(fields)
  
  	return new Promise(resolve => {
  		frappe.call("frappe.chat.doctype.chat_profile.chat_profile.create",
  			{ user: frappe.session.user, exists_ok: true, fields: fields },
  				response => {
  					if ( fn )
  						fn(response.message)
  
  					resolve(response.message)
  				})
  	})
  }
  
  /**
   * @description Updates a Chat Profile.
   *
   * @param   {string} user   - (Optional) Chat Profile User, defaults to session user.
   * @param   {object} update - (Required) Updates to be dispatched.
   *
   * @example
   * frappe.chat.profile.update(frappe.session.user, { "status": "Offline" })
   */
  frappe.chat.profile.update = (user, update, fn) => {
  	return new Promise(resolve => {
  		frappe.call("frappe.chat.doctype.chat_profile.chat_profile.update",
  			{ user: user || frappe.session.user, data: update },
  				response => {
  					if ( fn )
  						fn(response.message)
  
  					resolve(response.message)
  				})
  	})
  }
  
  // frappe.chat.profile.on
  frappe.provide('frappe.chat.profile.on')
  
  /**
   * @description Triggers on a Chat Profile update of a user (Only if there's a one-on-one conversation).
   *
   * @param   {function} fn - (Optional) callback with the User and the Chat Profile update.
   *
   * @returns {Promise}
   *
   * @example
   * frappe.chat.profile.on.update(function (user, update)
   * {
   *      // do stuff
   * })
   */
  frappe.chat.profile.on.update = function (fn) {
  	frappe.realtime.on("frappe.chat.profile:update", r => fn(r.user, r.data))
  }
  frappe.chat.profile.STATUSES
  =
  [
  	{
  		name: "Online",
  	   color: "green"
  	},
  	{
  		 name: "Away",
  		color: "yellow"
  	},
  	{
  		 name: "Busy",
  		color: "red"
  	},
  	{
  		 name: "Offline",
  		color: "darkgrey"
  	}
  ]
  
  // frappe.chat.room
  frappe.provide('frappe.chat.room')
  
  /**
   * @description Creates a Chat Room.
   *
   * @param   {string}       kind  - (Required) "Direct", "Group" or "Visitor".
   * @param   {string}       owner - (Optional) Chat Room owner (defaults to current user).
   * @param   {string|array} users - (Required for "Direct" and "Visitor", Optional for "Group") User(s) within Chat Room.
   * @param   {string}       name  - Chat Room name.
   * @param   {function}     fn    - callback with created Chat Room.
   *
   * @returns {Promise}
   *
   * @example
   * frappe.chat.room.create("Direct", frappe.session.user, "foo@bar.com", function (room) {
   *      // do stuff
   * })
   * frappe.chat.room.create("Group",  frappe.session.user, ["santa@gmail.com", "banta@gmail.com"], "Santa and Banta", function (room) {
   *      // do stuff
   * })
   */
  frappe.chat.room.create = function (kind, owner, users, name, fn) {
  	if ( typeof name === "function" ) {
  		fn   = name
  		name = null
  	}
  
  	users    = frappe._.as_array(users)
  
  	return new Promise(resolve => {
  		frappe.call("frappe.chat.doctype.chat_room.chat_room.create",
  			{ kind: kind, token: owner || frappe.session.user, users: users, name: name },
  			r => {
  				let room = r.message
  				room     = { ...room, creation: new frappe.datetime.datetime(room.creation) }
  
  				if ( fn )
  					fn(room)
  
  				resolve(room)
  			})
  	})
  }
  
  /**
   * @description Returns Chat Room(s).
   *
   * @param   {string|array} names   - (Optional) Chat Room(s) to retrieve.
   * @param   {string|array} fields  - (Optional) fields to be retrieved for each Chat Room.
   * @param   {function}     fn      - (Optional) callback with the returned Chat Room(s).
   *
   * @returns {Promise}
   *
   * @example
   * frappe.chat.room.get(function (rooms) {
   *      // do stuff
   * })
   * frappe.chat.room.get().then(function (rooms) {
   *      // do stuff
   * })
   *
   * frappe.chat.room.get(null, ["room_name", "avatar"], function (rooms) {
   *      // do stuff
   * })
   *
   * frappe.chat.room.get("CR00001", "room_name", function (room) {
   *      // do stuff
   * })
   *
   * frappe.chat.room.get(["CR00001", "CR00002"], ["room_name", "last_message"], function (rooms) {
   *
   * })
   */
  frappe.chat.room.get = function (names, fields, fn) {
  	if ( typeof names === "function" ) {
  		fn     = names
  		names  = null
  		fields = null
  	}
  	else
  	if ( typeof names === "string" ) {
  		names  = frappe._.as_array(names)
  
  		if ( typeof fields === "function" ) {
  			fn     = fields
  			fields = null
  		}
  		else
  		if ( typeof fields === "string" )
  			fields = frappe._.as_array(fields)
  	}
  
  	return new Promise(resolve => {
  		frappe.call("frappe.chat.doctype.chat_room.chat_room.get",
  			{ user: frappe.session.user, rooms: names, fields: fields },
  				response => {
  					let rooms = response.message
  					if ( rooms ) { // frappe.api BOGZ! (emtpy arrays are falsified, not good design).
  						rooms = frappe._.as_array(rooms)
  						rooms = rooms.map(room => {
  							return { ...room, creation: new frappe.datetime.datetime(room.creation),
  								last_message: room.last_message ? {
  									...room.last_message,
  									creation: new frappe.datetime.datetime(room.last_message.creation)
  								} : null
  							}
  						})
  						rooms = frappe._.squash(rooms)
  					}
  					else
  						rooms = [ ]
  
  					if ( fn )
  						fn(rooms)
  
  					resolve(rooms)
  				})
  	})
  }
  
  /**
   * @description Subscribe current user to said Chat Room(s).
   *
   * @param {string|array} rooms - Chat Room(s).
   *
   * @example
   * frappe.chat.room.subscribe("CR00001")
   */
  frappe.chat.room.subscribe = function (rooms) {
  	frappe.realtime.publish("frappe.chat.room:subscribe", rooms)
  }
  
  /**
   * @description Get Chat Room history.
   *
   * @param   {string} name - Chat Room name
   *
   * @returns {Promise}     - Chat Message(s)
   *
   * @example
   * frappe.chat.room.history(function (messages)
   * {
   *      // do stuff.
   * })
   */
  frappe.chat.room.history = function (name, fn) {
  	return new Promise(resolve => {
  		frappe.call("frappe.chat.doctype.chat_room.chat_room.history",
  			{ room: name, user: frappe.session.user },
  				r => {
  					let messages = r.message ? frappe._.as_array(r.message) : [ ] // frappe.api BOGZ! (emtpy arrays are falsified, not good design).
  					messages     = messages.map(m => {
  						return { ...m,
  							creation: new frappe.datetime.datetime(m.creation)
  						}
  					})
  
  					if ( fn )
  						fn(messages)
  
  					resolve(messages)
  				})
  	})
  }
  
  /**
   * @description Searches Rooms based on a query.
   *
   * @param   {string} query - The query string.
   * @param   {array}  rooms - A list of Chat Rooms.
   *
   * @returns {array}        - A fuzzy searched list of rooms.
   */
  frappe.chat.room.search = function (query, rooms) {
  	const dataset = rooms.map(r => {
  		if ( r.room_name )
  			return r.room_name
  		else
  			if ( r.owner === frappe.session.user )
  				return frappe.user.full_name(frappe._.squash(r.users))
  			else
  				return frappe.user.full_name(r.owner)
  	})
  	const results = frappe._.fuzzy_search(query, dataset)
  	rooms         = results.map(i => rooms[i])
  
  	return rooms
  }
  
  /**
   * @description Sort Chat Room(s) based on Last Message Timestamp or Creation Date.
   *
   * @param {array}   - A list of Chat Room(s)
   * @param {compare} - (Optional) a comparision function.
   */
  frappe.chat.room.sort = function (rooms, compare = null) {
  	compare = compare || function (a, b) {
  		if ( a.last_message && b.last_message )
  			return frappe.datetime.compare(a.last_message.creation, b.last_message.creation)
  		else
  		if ( a.last_message )
  			return frappe.datetime.compare(a.last_message.creation, b.creation)
  		else
  		if ( b.last_message )
  			return frappe.datetime.compare(a.creation, b.last_message.creation)
  		else
  			return frappe.datetime.compare(a.creation, b.creation)
  	}
  	rooms.sort(compare)
  
  	return rooms
  }
  
  // frappe.chat.room.on
  frappe.provide('frappe.chat.room.on')
  
  /**
   * @description Triggers on Chat Room updated.
   *
   * @param {function} fn - callback with the Chat Room and Update.
   */
  frappe.chat.room.on.update = function (fn) {
  	frappe.realtime.on("frappe.chat.room:update", r => {
  		if ( r.data.last_message )
  			// creation to frappe.datetime.datetime (easier to manipulate).
  			r.data = { ...r.data, last_message: { ...r.data.last_message, creation: new frappe.datetime.datetime(r.data.last_message.creation) } }
  
  		fn(r.room, r.data)
  	})
  }
  
  /**
   * @description Triggers on Chat Room created.
   *
   * @param {function} fn - callback with the created Chat Room.
   */
  frappe.chat.room.on.create = function (fn) {
  	frappe.realtime.on("frappe.chat.room:create", r =>
  		fn({ ...r, creation: new frappe.datetime.datetime(r.creation) })
  	)
  }
  
  /**
   * @description Triggers when a User is typing in a Chat Room.
   *
   * @param {function} fn - callback with the typing User within the Chat Room.
   */
  frappe.chat.room.on.typing = function (fn) {
  	frappe.realtime.on("frappe.chat.room:typing", r => fn(r.room, r.user))
  }
  
  // frappe.chat.message
  frappe.provide('frappe.chat.message')
  
  frappe.chat.message.typing = function (room, user) {
  	frappe.realtime.publish("frappe.chat.message:typing", { user: user || frappe.session.user, room: room })
  }
  
  frappe.chat.message.send   = function (room, message, type = "Content") {
  	frappe.call("frappe.chat.doctype.chat_message.chat_message.send",
  		{ user: frappe.session.user, room: room, content: message, type: type })
  }
  
  frappe.chat.message.update = function (message, update, fn) {
  	return new Promise(resolve => {
  		frappe.call('frappe.chat.doctype.chat_message.chat_message.update',
  			{ user: frappe.session.user, message: message, update: update },
  			r =>  {
  				if ( fn )
  					fn(response.message)
  
  				resolve(response.message)
  			})
  	})
  }
  
  frappe.chat.message.sort   = (messages) => {
  	if ( !frappe._.is_empty(messages) )
  		messages.sort((a, b) => frappe.datetime.compare(b.creation, a.creation))
  
  	return messages
  }
  
  /**
   * @description Add user to seen (defaults to session.user)
   */
  frappe.chat.message.seen   = (mess, user) => {
  	frappe.call('frappe.chat.doctype.chat_message.chat_message.seen',
  		{ message: mess, user: user || frappe.session.user })
  }
  
  frappe.provide('frappe.chat.message.on')
  frappe.chat.message.on.create = function (fn) {
  	frappe.realtime.on("frappe.chat.message:create", r =>
  		fn({ ...r, creation: new frappe.datetime.datetime(r.creation) })
  	)
  }
  
  frappe.chat.message.on.update = function (fn) {
  	frappe.realtime.on("frappe.chat.message:update", r => fn(r.message, r.data))
  }
  
  frappe.chat.pretty_datetime   = function (date) {
  	const today    = moment()
  	const instance = date.moment
  
  	if ( today.isSame(instance, "d") )
  		return instance.format("hh:mm A")
  	else
  	if ( today.isSame(instance, "week") )
  		return instance.format("dddd")
  	else
  		return instance.format("DD/MM/YYYY")
  }
  
  // frappe.chat.sound
  frappe.provide('frappe.chat.sound')
  
  /**
   * @description Plays a given registered sound.
   *
   * @param {value} - The name of the registered sound.
   *
   * @example
   * frappe.chat.sound.play("message")
   */
  frappe.chat.sound.play  = function (name, volume = 0.1) {
  	// frappe._.play_sound(`chat-${name}`)
  	const $audio = $(`<audio class="chat-audio"/>`)
  	$audio.attr('volume', volume)
  
  	if  ( frappe._.is_empty($audio) )
  		$(document).append($audio)
  
  	if  ( !$audio.paused ) {
  		frappe.log.info('Stopping sound playing.')
  		$audio[0].pause()
  		$audio.attr('currentTime', 0)
  	}
  
  	frappe.log.info('Playing sound.')
  	$audio.attr('src', `${frappe.chat.sound.PATH}/chat-${name}.mp3`)
  	$audio[0].play()
  }
  frappe.chat.sound.PATH  = '/assets/frappe/sounds'
  
  // frappe.chat.emoji
  frappe.chat.emojis = [ ]
  frappe.chat.emoji  = function (fn) {
  	return new Promise(resolve => {
  		if ( !frappe._.is_empty(frappe.chat.emojis) ) {
  			if ( fn )
  				fn(frappe.chat.emojis)
  
  			resolve(frappe.chat.emojis)
  		}
  		else
  			$.get('https://cdn.rawgit.com/frappe/emoji/master/emoji', (data) => {
  				frappe.chat.emojis = JSON.parse(data)
  
  				if ( fn )
  					fn(frappe.chat.emojis)
  
  				resolve(frappe.chat.emojis)
  			})
  	})
  }
  
  // Website Settings
  frappe.provide('frappe.chat.website.settings')
  frappe.chat.website.settings = (fields, fn) =>
  {
  	if ( typeof fields === "function" ) {
  		fn     = fields
  		fields = null
  	} else
  	if ( typeof fields === "string" )
  		fields = frappe._.as_array(fields)
  
  	return new Promise(resolve => {
  		frappe.call("frappe.chat.website.settings",
  			{ fields: fields })
  			.then(response => {
  				var message = response.message
  
  				if ( message.enable_from )
  					message   = { ...message, enable_from: new frappe.datetime.datetime(message.enable_from, 'HH:mm:ss') }
  				if ( message.enable_to )
  					message   = { ...message, enable_to:   new frappe.datetime.datetime(message.enable_to,   'HH:mm:ss') }
  
  				if ( fn )
  					fn(message)
  
  				resolve(message)
  			})
  	})
  }
  
  frappe.chat.website.token    = (fn) =>
  {
  	return new Promise(resolve => {
  		frappe.call("frappe.chat.website.token")
  			.then(response => {
  				if ( fn )
  					fn(response.message)
  
  				resolve(response.message)
  			})
  	})
  }
  
  const { h, Component } = hyper
  
  // frappe.components
  // frappe's component namespace.
  frappe.provide('frappe.components')
  
  frappe.provide('frappe.chat.component')
  
  /**
   * @description Button Component
   *
   * @prop {string}  type  - (Optional) "default", "primary", "info", "success", "warning", "danger" (defaults to "default")
   * @prop {boolean} block - (Optional) Render a button block (defaults to false).
   */
  frappe.components.Button
  =
  class extends Component {
  	render ( ) {
  		const { props } = this
  		const size      = frappe.components.Button.SIZE[props.size]
  
  		return (
  			h("button", { ...props, class: `btn ${size && size.class} btn-${props.type} ${props.block ? "btn-block" : ""} ${props.class ? props.class : ""}` },
  				props.children
  			)
  		)
  	}
  }
  frappe.components.Button.SIZE
  =
  {
  	small: {
  		class: "btn-sm"
  	},
  	large: {
  		class: "btn-lg"
  	}
  }
  frappe.components.Button.defaultProps
  =
  {
  	 type: "default",
  	block: false
  }
  
  /**
   * @description FAB Component
   *
   * @extends frappe.components.Button
   */
  frappe.components.FAB
  =
  class extends frappe.components.Button {
  	render ( ) {
  		const { props } = this
  		const size      = frappe.components.FAB.SIZE[props.size]
  
  		return (
  			h(frappe.components.Button, { ...props, class: `${props.class} ${size && size.class}`},
  				h("i", { class: props.icon })
  			)
  		)
  	}
  }
  frappe.components.FAB.defaultProps
  =
  {
  	icon: "octicon octicon-plus"
  }
  frappe.components.FAB.SIZE
  =
  {
  	small:
  	{
  		class: "frappe-fab-sm"
  	},
  	large:
  	{
  		class: "frappe-fab-lg"
  	}
  }
  
  /**
   * @description Octicon Component
   *
   * @prop color - (Required) color for the indicator
   */
  frappe.components.Indicator
  =
  class extends Component {
  	render ( ) {
  		const { props } = this
  
  		return props.color ? h("span", { ...props, class: `indicator ${props.color}` }) : null
  	}
  }
  
  /**
   * @description FontAwesome Component
   */
  frappe.components.FontAwesome
  =
  class extends Component {
  	render ( ) {
  		const { props } = this
  
  		return props.type ? h("i", { ...props, class: `fa ${props.fixed ? "fa-fw" : ""} fa-${props.type} ${props.class}` }) : null
  	}
  }
  frappe.components.FontAwesome.defaultProps
  =
  {
  	fixed: false
  }
  
  /**
   * @description Octicon Component
   *
   * @extends frappe.Component
   */
  frappe.components.Octicon
  =
  class extends Component {
  	render ( ) {
  		const { props } = this
  
  		return props.type ? h("i", { ...props, class: `octicon octicon-${props.type}` }) : null
  	}
  }
  
  /**
   * @description Avatar Component
   *
   * @prop {string} title - (Optional) title for the avatar.
   * @prop {string} abbr  - (Optional) abbreviation for the avatar, defaults to the first letter of the title.
   * @prop {string} size  - (Optional) size of the avatar to be displayed.
   * @prop {image}  image - (Optional) image for the avatar, defaults to the first letter of the title.
   */
  frappe.components.Avatar
  =
  class extends Component {
  	render ( ) {
  		const { props } = this
  		const abbr      = props.abbr || props.title.substr(0, 1)
  		const size      = frappe.components.Avatar.SIZE[props.size] || frappe.components.Avatar.SIZE.medium
  
  		return (
  			h("span", { class: `avatar ${size.class} ${props.class ? props.class : ""}` },
  				props.image ?
  					h("img", { class: "media-object", src: props.image })
  					:
  					h("div", { class: "standard-image" }, abbr)
  			)
  		)
  	}
  }
  frappe.components.Avatar.SIZE
  =
  {
  	small:
  	{
  		class: "avatar-small"
  	},
  	large:
  	{
  		class: "avatar-large"
  	},
  	medium:
  	{
  		class: "avatar-medium"
  	}
  }
  
  /**
   * @description Frappe Chat Object.
   *
   * @example
   * const chat = new frappe.Chat(options) // appends to "body"
   * chat.render()
   * const chat = new frappe.Chat(".selector", options)
   * chat.render()
   *
   * const chat = new frappe.Chat()
   * chat.set_wrapper('.selector')
   *     .set_options(options)
   *     .render()
   */
  frappe.Chat
  =
  class {
  	/**
  	 * @description Frappe Chat Object.
  	 *
  	 * @param {string} selector - A query selector, HTML Element or jQuery object.
  	 * @param {object} options  - Optional configurations.
  	 */
  	constructor (selector, options) {
  		if ( !(typeof selector === "string" || selector instanceof $ || selector instanceof HTMLElement) ) {
  			options  = selector
  			selector = null
  		}
  
  		this.options = frappe.Chat.OPTIONS
  
  		this.set_wrapper(selector ? selector : "body")
  		this.set_options(options)
  
  		// Load Emojis.
  		frappe.chat.emoji()
  	}
  
  	/**
  	 * Set the container on which the chat widget is mounted on.
  	 * @param   {string|HTMLElement} selector - A query selector, HTML Element or jQuery object.
  	 *
  	 * @returns {frappe.Chat}                 - The instance.
  	 *
  	 * @example
  	 * const chat = new frappe.Chat()
  	 * chat.set_wrapper(".selector")
  	 */
  	set_wrapper (selector) {
  		this.$wrapper = $(selector)
  
  		return this
  	}
  
  	/**
  	 * Set the configurations for the chat interface.
  	 * @param   {object}      options - Optional Configurations.
  	 *
  	 * @returns {frappe.Chat}         - The instance.
  	 *
  	 * @example
  	 * const chat = new frappe.Chat()
  	 * chat.set_options({ layout: frappe.Chat.Layout.PAGE })
  	 */
  	set_options (options) {
  		this.options = { ...this.options, ...options }
  
  		return this
  	}
  
  	/**
  	 * @description Destory the chat widget.
  	 *
  	 * @returns {frappe.Chat} - The instance.
  	 *
  	 * @example
  	 * const chat = new frappe.Chat()
  	 * chat.render()
  	 *     .destroy()
  	 */
  	destroy ( ) {
  		const $wrapper = this.$wrapper
  		$wrapper.remove(".frappe-chat")
  
  		return this
  	}
  
  	/**
  	 * @description Render the chat widget component onto destined wrapper.
  	 *
  	 * @returns {frappe.Chat} - The instance.
  	 *
  	 * @example
  	 * const chat = new frappe.Chat()
  	 * chat.render()
  	 */
  	render (props = { }) {
  		this.destroy()
  
  		const $wrapper   = this.$wrapper
  		const options    = this.options
  
  		const component  = h(frappe.Chat.Widget, {
  			layout: options.layout,
  			target: options.target,
  			...props
  		})
  
  		hyper.render(component, $wrapper[0])
  
  		return this
  	}
  }
  frappe.Chat.Layout
  =
  {
  	PAGE: "page", POPPER: "popper"
  }
  frappe.Chat.OPTIONS
  =
  {
  	layout: frappe.Chat.Layout.POPPER
  }
  
  /**
   * @description The base Component for Frappe Chat
   */
  frappe.Chat.Widget
  =
  class extends Component {
  	constructor (props) {
  		super (props)
  
  		this.setup(props)
  		this.make()
  	}
  
  	setup (props) {
  		// room actions
  		this.room           = { }
  		this.room.add       = rooms => {
  			rooms           = frappe._.as_array(rooms)
  			const names     = rooms.map(r => r.name)
  
  			frappe.log.info(`Subscribing ${frappe.session.user} to Chat Rooms ${names.join(", ")}.`)
  			frappe.chat.room.subscribe(names)
  
  			const state     = [ ]
  
  			for (const room of rooms)
  				  if ( ["Group", "Visitor"].includes(room.type) || room.owner === frappe.session.user || room.last_message || room.users.includes(frappe.session.user)) {
  					frappe.log.info(`Adding ${room.name} to component.`)
  					state.push(room)
  				}
  
  			this.set_state({ rooms: [ ...this.state.rooms, ...state ] })
  		}
  		this.room.update    = (room, update) => {
  			const { state } = this
  			var   exists    = false
  			const rooms     = state.rooms.map(r => {
  				if ( r.name === room ) {
  					exists  = true
  					if ( update.typing ) {
  						if ( !frappe._.is_empty(r.typing) ) {
  							const usr = update.typing
  							if ( !r.typing.includes(usr) ) {
  								update.typing = frappe._.copy_array(r.typing)
  								update.typing.push(usr)
  							}
  						}
  						else
  							update.typing = frappe._.as_array(update.typing)
  					}
  
  					return { ...r, ...update }
  				}
  
  				return r
  			})
  
  			if ( frappe.session.user !== 'Guest' ) {
  				if ( !exists )
  					frappe.chat.room.get(room, (room) => this.room.add(room))
  				else
  					this.set_state({ rooms })
  			}
  
  			if ( state.room.name === room ) {
  				if ( update.typing ) {
  					if ( !frappe._.is_empty(state.room.typing) ) {
  						const usr = update.typing
  						if ( !state.room.typing.includes(usr) ) {
  							update.typing = frappe._.copy_array(state.room.typing)
  							update.typing.push(usr)
  						}
  					} else
  						update.typing = frappe._.as_array(update.typing)
  				}
  
  				const room  = { ...state.room, ...update }
  
  				this.set_state({ room })
  			}
  		}
  		this.room.select    = (name) => {
  			frappe.chat.room.history(name, (messages) => {
  				const  { state } = this
  				const room       = state.rooms.find(r => r.name === name)
  
  				this.set_state({
  					room: { ...state.room, ...room, messages: messages }
  				})
  			})
  		}
  
  		this.state = { ...frappe.Chat.Widget.defaultState, ...props }
  	}
  
  	make ( ) {
  		if ( frappe.session.user !== 'Guest' ) {
  			frappe.chat.profile.create([
  				"status", "message_preview", "notification_tones", "conversation_tones"
  			]).then(profile => {
  				this.set_state({ profile })
  
  				frappe.chat.room.get(rooms => {
  					rooms = frappe._.as_array(rooms)
  					frappe.log.info(`User ${frappe.session.user} is subscribed to ${rooms.length} ${frappe._.pluralize('room', rooms.length)}.`)
  
  					if ( !frappe._.is_empty(rooms) )
  						this.room.add(rooms)
  				})
  
  				this.bind()
  			})
  		} else {
  			this.bind()
  		}
  	}
  
  	bind ( ) {
  		frappe.chat.profile.on.update((user, update) => {
  			frappe.log.warn(`TRIGGER: Chat Profile update ${JSON.stringify(update)} of User ${user}.`)
  
  			if ( 'status' in update ) {
  				if ( user === frappe.session.user ) {
  					this.set_state({
  						profile: { ...this.state.profile, status: update.status }
  					})
  				} else {
  					const status = frappe.chat.profile.STATUSES.find(s => s.name === update.status)
  					const color  = status.color
  
  					const alert  = `<span class="indicator ${color}"/> ${frappe.user.full_name(user)} is currently <b>${update.status}</b>`
  					frappe.show_alert(alert, 3)
  				}
  			}
  		})
  
  		frappe.chat.room.on.create((room) => {
  			frappe.log.warn(`TRIGGER: Chat Room ${room.name} created.`)
  			this.room.add(room)
  		})
  
  		frappe.chat.room.on.update((room, update) => {
  			frappe.log.warn(`TRIGGER: Chat Room ${room} update ${JSON.stringify(update)} recieved.`)
  			this.room.update(room, update)
  		})
  
  		frappe.chat.room.on.typing((room, user) => {
  			if ( user !== frappe.session.user ) {
  				frappe.log.warn(`User ${user} typing in Chat Room ${room}.`)
  				this.room.update(room, { typing: user })
  
  				setTimeout(() => this.room.update(room, { typing: null }), 5000)
  			}
  		})
  
  		frappe.chat.message.on.create((r) => {
  			const { state } = this
  
  			// play sound.
  			if ( state.room.name )
  				state.profile.conversation_tones && frappe.chat.sound.play('message')
  			else
  				state.profile.notification_tones && frappe.chat.sound.play('notification')
  
  			if ( r.user !== frappe.session.user && state.profile.message_preview && !state.toggle ) {
  				const $element = $('body').find('.frappe-chat-alert')
  				$element.remove()
  
  				const  alert   = // TODO: ellipses content
  				`
  				<span data-action="show-message" class="cursor-pointer">
  					<span class="indicator yellow"/>
  						<span class="avatar avatar-small">
  							<span class="avatar-frame" style="background-image: url(${frappe.user.image(r.user)})"></span>
  						</span>
  						<b>${frappe.user.first_name(r.user)}</b>: ${r.content}
  				</span>
  				`
  				frappe.show_alert(alert, 15, {
  					"show-message": function (r) {
  						this.room.select(r.room)
  						this.base.firstChild._component.toggle()
  					}.bind(this, r)
  				})
  				frappe.notify(`${frappe.user.first_name(r.user)}`, {
  					body: r.content,
  					icon: frappe.user.image(r.user),
  					tag: r.user
  				})
  			}
  
  			if ( r.room === state.room.name ) {
  				const mess  = frappe._.copy_array(state.room.messages)
  				mess.push(r)
  
  				this.set_state({ room: { ...state.room, messages: mess } })
  			}
  		})
  
  		frappe.chat.message.on.update((message, update) => {
  			frappe.log.warn(`TRIGGER: Chat Message ${message} update ${JSON.stringify(update)} recieved.`)
  		})
  	}
  
  	render ( ) {
  		const { props, state } = this
  		const me               = this
  
  		const ActionBar        = h(frappe.Chat.Widget.ActionBar, {
  			placeholder: __("Search or Create a New Chat"),
  				  class: "level",
  				 layout: props.layout,
  				actions:
  			frappe._.compact([
  				{
  					  label: __("New"),
  					onclick: function ( ) {
  						const dialog = new frappe.ui.Dialog({
  							  title: __("New Chat"),
  							 fields: [
  								 {
  										 label: __("Chat Type"),
  									 fieldname: "type",
  									 fieldtype: "Select",
  									   options: ["Group", "Direct Chat"],
  									   default: "Group",
  									  onchange: () =>  {
  											const type     = dialog.get_value("type")
  											const is_group = type === "Group"
  
  											dialog.set_df_property("group_name", "reqd",  is_group)
  											dialog.set_df_property("user",       "reqd", !is_group)
  									  }
  								 },
  								 {
  										 label: __("Group Name"),
  									 fieldname: "group_name",
  									 fieldtype: "Data",
  										  reqd: true,
  									depends_on: "eval:doc.type == 'Group'"
  								 },
  								 {
  										 label: __("Users"),
  									 fieldname: "users",
  									 fieldtype: "MultiSelect",
  									   options: frappe.user.get_emails(),
  									depends_on: "eval:doc.type == 'Group'"
  								 },
  								 {
  										 label: __("User"),
  									 fieldname: "user",
  									 fieldtype: "Link",
  									   options: "User",
  									depends_on: "eval:doc.type == 'Direct Chat'"
  								 }
  							 ],
  							action: {
  								primary: {
  									   label: __('Create'),
  									onsubmit: (values) => {
  										if ( values.type === "Group" ) {
  											if ( !frappe._.is_empty(values.users) ) {
  												const name  = values.group_name
  												const users = dialog.fields_dict.users.get_values()
  
  												frappe.chat.room.create("Group",  null, users, name)
  											}
  										} else {
  											const user      = values.user
  
  											frappe.chat.room.create("Direct", null, user)
  										}
  										dialog.hide()
  									}
  								}
  							}
  						})
  						dialog.show()
  					}
  				},
  				frappe._.is_mobile() && {
  					   icon: "octicon octicon-x",
  					   class: "frappe-chat-close",
  					onclick: () => this.set_state({ toggle: false })
  				}
  			], Boolean),
  			change: query => { me.set_state({ query }) },
  			  span: span  => { me.set_state({ span  }) },
  		})
  
  		var   contacts   = [ ]
  		if ( 'user_info' in frappe.boot ) {
  			const emails = frappe.user.get_emails()
  			for (const email of emails) {
  				var exists = false
  
  				for (const room of state.rooms) {
  					if ( room.type === 'Direct' ) {
  						if ( room.owner === email || frappe._.squash(room.users) === email )
  							exists = true
  					}
  				}
  
  				if ( !exists )
  					contacts.push({ owner: frappe.session.user, users: [email] })
  			}
  		}
  		const rooms      = state.query ? frappe.chat.room.search(state.query, state.rooms.concat(contacts)) : frappe.chat.room.sort(state.rooms)
  
  		const layout     = state.span  ? frappe.Chat.Layout.PAGE : frappe.Chat.Layout.POPPER
  
  		const RoomList   = frappe._.is_empty(rooms) && !state.query ?
  			h("div", { class: "vcenter" },
  				h("div", { class: "text-center text-extra-muted" },
  					h("p","",__("You don't have any messages yet."))
  				)
  			)
  			:
  			h(frappe.Chat.Widget.RoomList, { rooms: rooms, click: room =>  {
  				if ( room.name )
  					this.room.select(room.name)
  				else
  					frappe.chat.room.create("Direct", room.owner, frappe._.squash(room.users), ({ name }) => this.room.select(name))
  			}})
  		const Room       = h(frappe.Chat.Widget.Room, { ...state.room, layout: layout, destroy: () => {
  			this.set_state({
  				room: { name: null, messages: [ ] }
  			})
  		}})
  
  		const component  = layout === frappe.Chat.Layout.POPPER ?
  			h(frappe.Chat.Widget.Popper, { heading: ActionBar, page: state.room.name && Room, target: props.target,
  				toggle: (t) => this.set_state({ toggle: t }) },
  				RoomList
  			)
  			:
  			h("div", { class: "frappe-chat-popper" },
  				h("div", { class: "frappe-chat-popper-collapse" },
  					h("div", { class: "panel panel-default panel-span", style: { width: "25%" } },
  						h("div", { class: "panel-heading" },
  							ActionBar
  						),
  						RoomList
  					),
  					Room
  				)
  			)
  
  		return (
  			h("div", { class: "frappe-chat" },
  				component
  			)
  		)
  	}
  }
  frappe.Chat.Widget.defaultState =  {
  	  query: "",
  	profile: { },
  	  rooms: [ ],
  	   room: { name: null, messages: [ ], typing: [ ] },
  	 toggle: false,
  	   span: false
  }
  frappe.Chat.Widget.defaultProps = {
  	layout: frappe.Chat.Layout.POPPER
  }
  
  /**
   * @description Chat Widget Popper HOC.
   */
  frappe.Chat.Widget.Popper
  =
  class extends Component {
  	constructor (props) {
  		super (props)
  
  		this.setup(props);
  	}
  
  	setup (props) {
  		this.toggle = this.toggle.bind(this)
  
  		this.state  = frappe.Chat.Widget.Popper.defaultState
  
  		if ( props.target )
  			$(props.target).click(() => this.toggle())
  
  		frappe.chat.widget = this
  	}
  
  	toggle  (active) {
  		let toggle
  		if ( arguments.length === 1 )
  			toggle = active
  		else
  			toggle = this.state.active ? false : true
  
  		this.set_state({ active: toggle })
  
  		this.props.toggle(toggle)
  	}
  
  	on_mounted ( ) {
  		$(document.body).on('click', '.page-container, .frappe-chat-close', ({ currentTarget }) => {
  			this.toggle(false)
  		})
  	}
  
  	render  ( )  {
  		const { props, state } = this
  
  		return !state.destroy ?
  		(
  			h("div", { class: "frappe-chat-popper", style: !props.target ? { "margin-bottom": "80px" } : null },
  				!props.target ?
  					h(frappe.components.FAB, {
  						  class: "frappe-fab",
  						   icon: state.active ? "fa fa-fw fa-times" : "font-heavy octicon octicon-comment",
  						   size: frappe._.is_mobile() ? null : "large",
  						   type: "primary",
  						onclick: () => this.toggle(),
  					}) : null,
  				state.active ?
  					h("div", { class: "frappe-chat-popper-collapse" },
  						props.page ? props.page : (
  							h("div", { class: `panel panel-default ${frappe._.is_mobile() ? "panel-span" : ""}` },
  								h("div", { class: "panel-heading" },
  									props.heading
  								),
  								props.children
  							)
  						)
  				) : null
  			)
  		) : null
  	}
  }
  frappe.Chat.Widget.Popper.defaultState
  =
  {
  	 active: false,
  	destroy: false
  }
  
  /**
   * @description frappe.Chat.Widget ActionBar Component
   */
  frappe.Chat.Widget.ActionBar
  =
  class extends Component {
  	constructor (props) {
  		super (props)
  
  		this.change = this.change.bind(this)
  		this.submit = this.submit.bind(this)
  
  		this.state  = frappe.Chat.Widget.ActionBar.defaultState
  	}
  
  	change (e) {
  		const { props, state } = this
  
  		this.set_state({
  			[e.target.name]: e.target.value
  		})
  
  		props.change(state.query)
  	}
  
  	submit (e) {
  		const { props, state } = this
  
  		e.preventDefault()
  
  		props.submit(state.query)
  	}
  
  	render ( ) {
  		const me               = this
  		const { props, state } = this
  		const { actions }      = props
  
  		return (
  			h("div", { class: `frappe-chat-action-bar ${props.class ? props.class : ""}` },
  				h("form", { oninput: this.change, onsubmit: this.submit },
  					h("input", { autocomplete: "off", class: "form-control input-sm", name: "query", value: state.query, placeholder: props.placeholder || "Search" }),
  				),
  				!frappe._.is_empty(actions) ?
  					actions.map(action => h(frappe.Chat.Widget.ActionBar.Action, { ...action })) : null,
  				!frappe._.is_mobile() ?
  					h(frappe.Chat.Widget.ActionBar.Action, {
  						icon: `octicon octicon-screen-${state.span ? "normal" : "full"}`,
  						onclick: () => {
  							const span = !state.span
  							me.set_state({ span })
  							props.span(span)
  						}
  					})
  					:
  					null
  			)
  		)
  	}
  }
  frappe.Chat.Widget.ActionBar.defaultState
  =
  {
  	query: null,
  	 span: false
  }
  
  /**
   * @description frappe.Chat.Widget ActionBar's Action Component.
   */
  frappe.Chat.Widget.ActionBar.Action
  =
  class extends Component {
  	render ( ) {
  		const { props } = this
  
  		return (
  			h(frappe.components.Button, { size: "small", class: "btn-action", ...props },
  				props.icon ? h("i", { class: props.icon }) : null,
  				`${props.icon ? " " : ""}${props.label ? props.label : ""}`
  			)
  		)
  	}
  }
  
  /**
   * @description frappe.Chat.Widget RoomList Component
   */
  frappe.Chat.Widget.RoomList
  =
  class extends Component {
  	render ( ) {
  		const { props } = this
  		const rooms     = props.rooms
  
  		return !frappe._.is_empty(rooms) ? (
  			h("ul", { class: "frappe-chat-room-list nav nav-pills nav-stacked" },
  				rooms.map(room => h(frappe.Chat.Widget.RoomList.Item, { ...room, click: props.click }))
  			)
  		) : null
  	}
  }
  
  /**
   * @description frappe.Chat.Widget RoomList's Item Component
   */
  frappe.Chat.Widget.RoomList.Item
  =
  class extends Component {
  	render ( ) {
  		const { props }    = this
  		const item         = { }
  
  		if ( props.type === "Group" ) {
  			item.title     = props.room_name
  			item.image     = props.avatar
  
  			if ( !frappe._.is_empty(props.typing) ) {
  				props.typing  = frappe._.as_array(props.typing) // HACK: (BUG) why does typing return a string?
  				const names   = props.typing.map(user => frappe.user.first_name(user))
  				item.subtitle = `${names.join(", ")} typing...`
  			} else
  			if ( props.last_message ) {
  				const message = props.last_message
  				const content = message.content
  
  				if ( message.type === "File" ) {
  					item.subtitle = `📁 ${content.name}`
  				} else {
  					item.subtitle = props.last_message.content
  				}
  			}
  		} else {
  			const user     = props.owner === frappe.session.user ? frappe._.squash(props.users) : props.owner
  
  			item.title     = frappe.user.full_name(user)
  			item.image     = frappe.user.image(user)
  			item.abbr      = frappe.user.abbr(user)
  
  			if ( !frappe._.is_empty(props.typing) )
  				item.subtitle = 'typing...'
  			else
  			if ( props.last_message ) {
  				const message = props.last_message
  				const content = message.content
  
  				if ( message.type === "File" ) {
  					item.subtitle = `📁 ${content.name}`
  				} else {
  					item.subtitle = props.last_message.content
  				}
  			}
  		}
  
  		let is_unread = false
  		if ( props.last_message ) {
  			item.timestamp = frappe.chat.pretty_datetime(props.last_message.creation)
  			is_unread = !props.last_message.seen.includes(frappe.session.user)
  		}
  
  		return (
  			h("li", null,
  				h("a", { class: props.active ? "active": "", onclick: () => {
  					if (props.last_message) {
  						frappe.chat.message.seen(props.last_message.name);
  					}
  					props.click(props)
  				} },
  					h("div", { class: "row" },
  						h("div", { class: "col-xs-9" },
  							h(frappe.Chat.Widget.MediaProfile, { ...item })
  						),
  						h("div", { class: "col-xs-3 text-right" },
  							[
  								h("div", { class: "text-muted", style: { "font-size": "9px" } }, item.timestamp),
  								is_unread ? h("span", { class: "indicator red" }) : null
  							]
  						),
  					)
  				)
  			)
  		)
  	}
  }
  
  /**
   * @description frappe.Chat.Widget's MediProfile Component.
   */
  frappe.Chat.Widget.MediaProfile
  =
  class extends Component {
  	render ( ) {
  		const { props } = this
  		const position  = frappe.Chat.Widget.MediaProfile.POSITION[props.position || "left"]
  		const avatar    = (
  			h("div", { class: `${position.class} media-middle` },
  				h(frappe.components.Avatar, { ...props,
  					title: props.title,
  					image: props.image,
  					 size: props.size,
  					 abbr: props.abbr
  				})
  			)
  		)
  
  		return (
  			h("div", { class: "media", style: position.class === "media-right" ? { "text-align": "right" } : null },
  				position.class === "media-left"  ? avatar : null,
  				h("div", { class: "media-body" },
  					h("div", { class: "media-heading ellipsis small", style: `max-width: ${props.width_title || "100%"} display: inline-block` }, props.title),
  					props.content  ? h("div","",h("small","",props.content))  : null,
  					props.subtitle ? h("div",{ class: "media-subtitle small" },h("small", { class: "text-muted" }, props.subtitle)) : null
  				),
  				position.class === "media-right" ? avatar : null
  			)
  		)
  	}
  }
  frappe.Chat.Widget.MediaProfile.POSITION
  =
  {
  	left: { class: "media-left" }, right: { class: "media-right" }
  }
  
  /**
   * @description frappe.Chat.Widget Room Component
   */
  frappe.Chat.Widget.Room
  =
  class extends Component {
  	render ( ) {
  		const { props, state } = this
  		const hints            =
  		[
  			{
  				 match: /@(\w*)$/,
  				search: function (keyword, callback) {
  					if ( props.type === 'Group' ) {
  						const query = keyword.slice(1)
  						const users = [].concat(frappe._.as_array(props.owner), props.users)
  						const grep  = users.filter(user => user !== frappe.session.user && user.indexOf(query) === 0)
  
  						callback(grep)
  					}
  				},
  				component: function (item) {
  					return (
  						h(frappe.Chat.Widget.MediaProfile, {
  							title: frappe.user.full_name(item),
  							image: frappe.user.image(item),
  							 size: "small"
  						})
  					)
  				}
  			},
  			{
  				match: /:([a-z]*)$/,
  			   search: function (keyword, callback) {
  					frappe.chat.emoji(function (emojis) {
  						const query = keyword.slice(1)
  						const items = [ ]
  						for (const emoji of emojis)
  							for (const alias of emoji.aliases)
  								if ( alias.indexOf(query) === 0 )
  									items.push({ name: alias, value: emoji.emoji })
  
  						callback(items)
  					})
  			   },
  				 content: (item) => item.value,
  			   component: function (item) {
  					return (
  						h(frappe.Chat.Widget.MediaProfile, {
  							title: item.name,
  							 abbr: item.value,
  							 size: "small"
  						})
  					)
  			   }
  		   }
  		]
  
  		const actions = frappe._.compact([
  			!frappe._.is_mobile() && {
  				 icon: "camera",
  				label: "Camera",
  				onclick: ( ) => {
  					const capture = new frappe.ui.Capture({
  						animate: false,
  						  error: true
  					})
  					capture.show()
  
  					capture.submit(data_url => {
  						// data_url
  					})
  				}
  			},
  			{
  				 icon: "file",
  				label: "File",
  				onclick: ( ) => {
  					new frappe.ui.FileUploader({
  						doctype: "Chat Room",
  						docname: props.name,
  						on_success(file_doc) {
  							const { file_url, filename } = file_doc
  							frappe.chat.message.send(props.name, { path: file_url, name: filename }, "File")
  						}
  					})
  				}
  			}
  		])
  
  		if ( frappe.session.user !== 'Guest' ) {
  			if (props.messages) {
  				props.messages = frappe._.as_array(props.messages)
  				for (const message of props.messages)
  					if ( !message.seen.includes(frappe.session.user) )
  						frappe.chat.message.seen(message.name)
  					else
  						break
  			}
  		}
  
  		return (
  			h("div", { class: `panel panel-default
  				${props.name ? "panel-bg" : ""}
  				${props.layout === frappe.Chat.Layout.PAGE || frappe._.is_mobile() ? "panel-span" : ""}`,
  				style: props.layout === frappe.Chat.Layout.PAGE && { width: "75%", left: "25%", "box-shadow": "none" } },
  				props.name && h(frappe.Chat.Widget.Room.Header, { ...props, on_back: props.destroy }),
  				props.name ?
  					!frappe._.is_empty(props.messages) ?
  						h(frappe.chat.component.ChatList, {
  							messages: props.messages
  						})
  						:
  						h("div", { class: "panel-body", style: { "height": "100%" } },
  							h("div", { class: "vcenter" },
  								h("div", { class: "text-center text-extra-muted" },
  									h(frappe.components.Octicon, { type: "comment-discussion", style: "font-size: 48px" }),
  									h("p","",__("Start a conversation."))
  								)
  							)
  						)
  					:
  					h("div", { class: "panel-body", style: { "height": "100%" } },
  						h("div", { class: "vcenter" },
  							h("div", { class: "text-center text-extra-muted" },
  								h(frappe.components.Octicon, { type: "comment-discussion", style: "font-size: 125px" }),
  								h("p","",__("Select a chat to start messaging."))
  							)
  						)
  					),
  				props.name ?
  					h("div", { class: "chat-room-footer" },
  						h(frappe.chat.component.ChatForm, { actions: actions,
  							onchange: () => {
  								frappe.chat.message.typing(props.name)
  							},
  							onsubmit: (message) => {
  								frappe.chat.message.send(props.name, message)
  							},
  							hint: hints
  						})
  					)
  					:
  					null
  			)
  		)
  	}
  }
  
  frappe.Chat.Widget.Room.Header
  =
  class extends Component {
  	render ( ) {
  		const { props }     = this
  
  		const item          = { }
  
  		if ( ["Group", "Visitor"].includes(props.type) ) {
  			item.route      = `Form/Chat Room/${props.name}`
  
  			item.title      = props.room_name
  			item.image      = props.avatar
  
  			if ( !frappe._.is_empty(props.typing) ) {
  				props.typing  = frappe._.as_array(props.typing) // HACK: (BUG) why does typing return as a string?
  				const users   = props.typing.map(user => frappe.user.first_name(user))
  				item.subtitle = `${users.join(", ")} typing...`
  			} else
  				item.subtitle = props.type === "Group" ?
  					__(`${props.users.length} ${frappe._.pluralize('member', props.users.length)}`)
  					:
  					""
  		}
  		else {
  			const user      = props.owner === frappe.session.user ? frappe._.squash(props.users) : props.owner
  
  			item.route      = `Form/User/${user}`
  
  			item.title      = frappe.user.full_name(user)
  			item.image      = frappe.user.image(user)
  
  			if ( !frappe._.is_empty(props.typing) )
  				item.subtitle = 'typing...'
  		}
  
  		const popper        = props.layout === frappe.Chat.Layout.POPPER || frappe._.is_mobile()
  
  		return (
  			h("div", { class: "panel-heading", style: { "height": "50px" } }, // sorry. :(
  				h("div", { class: "level" },
  					popper && frappe.session.user !== "Guest" ?
  						h(frappe.components.Button,{class:"btn-back",onclick:props.on_back},
  							h(frappe.components.Octicon, { type: "chevron-left" })
  						) : null,
  					h("div","",
  						h("div", { class: "panel-title" },
  							h("div", { class: "cursor-pointer", onclick: () => { frappe.set_route(item.route) }},
  								h(frappe.Chat.Widget.MediaProfile, { ...item })
  							)
  						)
  					),
  					h("div", { class: popper ? "col-xs-1"  : "col-xs-3" },
  						h("div", { class: "text-right" },
  
  						)
  					)
  				)
  			)
  		)
  	}
  }
  
  /**
   * @description ChatList Component
   *
   * @prop {array} messages - ChatMessage(s)
   */
  frappe.chat.component.ChatList
  =
  class extends Component {
  	on_mounted ( ) {
  		this.$element  = $('.frappe-chat').find('.chat-list')
  		this.$element.scrollTop(this.$element[0].scrollHeight)
  	}
  
  	on_updated ( ) {
  		this.$element.scrollTop(this.$element[0].scrollHeight)
  	}
  
  	render ( ) {
  		var messages = [ ]
  		for (var i   = 0 ; i < this.props.messages.length ; ++i) {
  			var   message   = this.props.messages[i]
  			const me        = message.user === frappe.session.user
  
  			if ( i === 0 || !frappe.datetime.equal(message.creation, this.props.messages[i - 1].creation, 'day') )
  				messages.push({ type: "Notification", content: message.creation.format('MMMM DD') })
  
  			messages.push(message)
  		}
  
  		return (
  			h("div",{class:"chat-list list-group"},
  				!frappe._.is_empty(messages) ?
  					messages.map(m => h(frappe.chat.component.ChatList.Item, {...m})) : null
  			)
  		)
  	}
  }
  
  /**
   * @description ChatList.Item Component
   *
   * @prop {string} name       - ChatMessage name
   * @prop {string} user       - ChatMessage user
   * @prop {string} room       - ChatMessage room
   * @prop {string} room_type  - ChatMessage room_type ("Direct", "Group" or "Visitor")
   * @prop {string} content    - ChatMessage content
   * @prop {frappe.datetime.datetime} creation - ChatMessage creation
   *
   * @prop {boolean} groupable - Whether the ChatMessage is groupable.
   */
  frappe.chat.component.ChatList.Item
  =
  class extends Component {
  	render ( ) {
  		const { props } = this
  
  		const me        = props.user === frappe.session.user
  		const content   = props.content
  
  		return (
  			h("div",{class: "chat-list-item list-group-item"},
  				props.type === "Notification" ?
  					h("div",{class:"chat-list-notification"},
  						h("div",{class:"chat-list-notification-content"},
  							content
  						)
  					)
  					:
  					h("div",{class:`${me ? "text-right" : ""}`},
  						props.room_type === "Group" && !me ?
  							h(frappe.components.Avatar, {
  								title: frappe.user.full_name(props.user),
  								image: frappe.user.image(props.user)
  							}) : null,
  						h(frappe.chat.component.ChatBubble, props)
  					)
  			)
  		)
  	}
  }
  
  /**
   * @description ChatBubble Component
   *
   * @prop {string} name       - ChatMessage name
   * @prop {string} user       - ChatMessage user
   * @prop {string} room       - ChatMessage room
   * @prop {string} room_type  - ChatMessage room_type ("Direct", "Group" or "Visitor")
   * @prop {string} content    - ChatMessage content
   * @prop {frappe.datetime.datetime} creation - ChatMessage creation
   *
   * @prop {boolean} groupable - Whether the ChatMessage is groupable.
   */
  frappe.chat.component.ChatBubble
  =
  class extends Component {
  	constructor (props) {
  		super (props)
  
  		this.onclick = this.onclick.bind(this)
  	}
  
  	onclick ( ) {
  		const { props } = this
  		if ( props.user === frappe.session.user ) {
  			frappe.quick_edit("Chat Message", props.name, (values) => {
  
  			})
  		}
  	}
  
  	render  ( ) {
  		const { props } = this
  		const creation 	= props.creation.format('hh:mm A')
  
  		const me        = props.user === frappe.session.user
  		const read      = !frappe._.is_empty(props.seen) && !props.seen.includes(frappe.session.user)
  
  		const content   = props.content
  
  		return (
  			h("div",{class:`chat-bubble ${props.groupable ? "chat-groupable" : ""} chat-bubble-${me ? "r" : "l"}`,
  				onclick: this.onclick},
  				props.room_type === "Group" && !me?
  					h("div",{class:"chat-bubble-author"},
  						h("a", { onclick: () => { frappe.set_route(`Form/User/${props.user}`) } },
  							frappe.user.full_name(props.user)
  						)
  					) : null,
  				h("div",{class:"chat-bubble-content"},
  						h("small","",
  							props.type === "File" ?
  								h("a", { class: "no-decoration", href: content.path, target: "_blank" },
  									h(frappe.components.FontAwesome, { type: "file", fixed: true }), ` ${content.name}`
  								)
  								:
  								content
  						)
  				),
  				h("div",{class:"chat-bubble-meta"},
  					h("span",{class:"chat-bubble-creation"},creation),
  					me && read ?
  						h("span",{class:"chat-bubble-check"},
  							h(frappe.components.Octicon,{type:"check"})
  						) : null
  				)
  			)
  		)
  	}
  }
  
  /**
   * @description ChatForm Component
   */
  frappe.chat.component.ChatForm
  =
  class extends Component {
  	constructor (props) {
  		super (props)
  
  		this.onchange   = this.onchange.bind(this)
  		this.onsubmit   = this.onsubmit.bind(this)
  
  		this.hint        = this.hint.bind(this)
  
  		this.state       = frappe.chat.component.ChatForm.defaultState
  	}
  
  	onchange (e) {
  		const { props, state } = this
  		const value            = e.target.value
  
  		this.set_state({
  			[e.target.name]: value
  		})
  
  		props.onchange(state)
  
  		this.hint(value)
  	}
  
  	hint (value) {
  		const { props, state } = this
  
  		if ( props.hint ) {
  			const tokens =  value.split(" ")
  			const sliced = tokens.slice(0, tokens.length - 1)
  
  			const token  = tokens[tokens.length - 1]
  
  			if ( token ) {
  				props.hint   = frappe._.as_array(props.hint)
  				const hint   = props.hint.find(hint => hint.match.test(token))
  
  				if ( hint ) {
  					hint.search(token, items => {
  						const hints = items.map(item => {
  							// You should stop writing one-liners! >_>
  							const replace = token.replace(hint.match, hint.content ? hint.content(item) : item)
  							const content = `${sliced.join(" ")} ${replace}`.trim()
  							item          = { component: hint.component(item), content: content }
  
  							return item
  						}).slice(0, hint.max || 5)
  
  						this.set_state({ hints })
  					})
  				}
  				else
  					this.set_state({ hints: [ ] })
  			} else
  				this.set_state({ hints: [ ] })
  		}
  	}
  
  	onsubmit (e) {
  		e.preventDefault()
  
  		if ( this.state.content ) {
  			this.props.onsubmit(this.state.content)
  
  			this.set_state({ content: null })
  		}
  	}
  
  	render ( ) {
  		const { props, state } = this
  
  		return (
  			h("div",{class:"chat-form"},
  				state.hints.length ?
  					h("ul", { class: "hint-list list-group" },
  						state.hints.map((item) => {
  							return (
  								h("li", { class: "hint-list-item list-group-item" },
  									h("a", { href: "javascript:void(0)", onclick: () => {
  										this.set_state({ content: item.content, hints: [ ] })
  									}},
  										item.component
  									)
  								)
  							)
  						})
  					) : null,
  				h("form", { oninput: this.onchange, onsubmit: this.onsubmit },
  					h("div",{class:"input-group input-group-lg"},
  						!frappe._.is_empty(props.actions) ?
  							h("div",{class:"input-group-btn dropup"},
  								h(frappe.components.Button,{ class: (frappe.session.user === "Guest" ? "disabled" : "dropdown-toggle"), "data-toggle": "dropdown"},
  									h(frappe.components.FontAwesome, { class: "text-muted", type: "paperclip", fixed: true })
  								),
  								h("div",{ class:"dropdown-menu dropdown-menu-left", onclick: e => e.stopPropagation() },
  									!frappe._.is_empty(props.actions) && props.actions.map((action) => {
  										return (
  											h("li", null,
  												h("a",{onclick:action.onclick},
  													h(frappe.components.FontAwesome,{type:action.icon,fixed:true}), ` ${action.label}`,
  												)
  											)
  										)
  									})
  								)
  							) : null,
  						h("textarea", {
  									class: "form-control",
  									 name: "content",
  									value: state.content,
  							  placeholder: "Type a message",
  								autofocus: true,
  							   onkeypress: (e) => {
  									if ( e.which === frappe.ui.keycode.RETURN && !e.shiftKey )
  										this.onsubmit(e)
  							   }
  						}),
  						h("div",{class:"input-group-btn"},
  							h(frappe.components.Button, { onclick: this.onsubmit },
  								h(frappe.components.FontAwesome, { class: !frappe._.is_empty(state.content) ? "text-primary" : "text-muted", type: "send", fixed: true })
  							),
  						)
  					)
  				)
  			)
  		)
  	}
  }
  frappe.chat.component.ChatForm.defaultState
  =
  {
  	content: null,
  	  hints: [ ],
  }
  
  /**
   * @description EmojiPicker Component
   *
   * @todo Under Development
   */
  frappe.chat.component.EmojiPicker
  =
  class extends Component  {
  	render ( ) {
  		const { props } = this
  
  		return (
  			h("div", { class: `frappe-chat-emoji dropup ${props.class}` },
  				h(frappe.components.Button, { type: "primary", class: "dropdown-toggle", "data-toggle": "dropdown" },
  					h(frappe.components.FontAwesome, { type: "smile-o", fixed: true })
  				),
  				h("div", { class: "dropdown-menu dropdown-menu-right", onclick: e => e.stopPropagation() },
  					h("div", { class: "panel panel-default" },
  						h(frappe.chat.component.EmojiPicker.List)
  					)
  				)
  			)
  		)
  	}
  }
  frappe.chat.component.EmojiPicker.List
  =
  class extends Component {
  	render ( ) {
  		const { props } = this
  
  		return (
  			h("div", { class: "list-group" },
  
  			)
  		)
  	}
  }
  
  /**
   * @description Python equivalent to sys.platform
   */
  frappe.provide('frappe._')
  frappe._.platform   = () => {
  	const string    = navigator.appVersion
  
  	if ( string.includes("Win") ) 	return "Windows"
  	if ( string.includes("Mac") ) 	return "Darwin"
  	if ( string.includes("X11") ) 	return "UNIX"
  	if ( string.includes("Linux") ) return "Linux"
  
  	return undefined
  }
  
  /**
   * @description Frappe's Asset Helper
   */
  frappe.provide('frappe.assets')
  frappe.assets.image = (image, app = 'frappe') => {
  	const  path     = `/assets/${app}/images/${image}`
  	return path
  }
  
  /**
   * @description Notify using Web Push Notifications
   */
  frappe.provide('frappe.boot')
  frappe.provide('frappe.browser')
  frappe.browser.Notification = 'Notification' in window
  
  frappe.notify     = (string, options) => {
  	frappe.log    = frappe.Logger.get('frappe.notify')
  
  	const OPTIONS = {
  		icon: frappe.assets.image('favicon.png', 'frappe'),
  		lang: frappe.boot.lang || "en"
  	}
  	options       = Object.assign({ }, OPTIONS, options)
  
  	if ( !frappe.browser.Notification )
  		frappe.log.error('ERROR: This browser does not support desktop notifications.')
  
  	Notification.requestPermission(status => {
  		if ( status === "granted" ) {
  			const notification = new Notification(string, options)
  		}
  	})
  }
  
  frappe.chat.render = (render = true, force = false) =>
  {
  	frappe.log.info(`${render ? "Enable" : "Disable"} Chat for User.`)
  
  	const desk = 'desk' in frappe
  	if ( desk ) {
  		// With the assumption, that there's only one navbar.
  		const $placeholder = $('.navbar .frappe-chat-dropdown')
  
  		// Render if frappe-chat-toggle doesn't exist.
  		if ( frappe.utils.is_empty($placeholder.has('.frappe-chat-toggle')) ) {
  			const $template = $(`
  				<a class="dropdown-toggle frappe-chat-toggle" data-toggle="dropdown">
  					<div>
  						<i class="octicon octicon-comment-discussion"/>
  					</div>
  				</a>
  			`)
  
  			$placeholder.addClass('dropdown hidden')
  			$placeholder.html($template)
  		}
  
  		if ( render ) {
  			$placeholder.removeClass('hidden')
  		} else {
  			$placeholder.addClass('hidden')
  		}
  	}
  
  	// Avoid re-renders. Once is enough.
  	if ( !frappe.chatter || force ) {
  		frappe.chatter = new frappe.Chat({
  			target: desk ? '.frappe-chat-toggle' : null
  		})
  
  		if ( render ) {
  			if ( frappe.session.user === 'Guest' && !desk ) {
  				frappe.store = frappe.Store.get('frappe.chat')
  				var token	 = frappe.store.get('guest_token')
  
  				frappe.log.info(`Local Guest Token - ${token}`)
  
  				const setup_room = (token) =>
  				{
  					return new Promise(resolve => {
  						frappe.chat.room.create("Visitor", token).then(room => {
  							frappe.log.info(`Visitor Room Created: ${room.name}`)
  							frappe.chat.room.subscribe(room.name)
  
  							var reference = room
  
  							frappe.chat.room.history(room.name).then(messages => {
  								const  room = { ...reference, messages: messages }
  								return room
  							}).then(room => {
  								resolve(room)
  							})
  						})
  					})
  				}
  
  				if ( !token ) {
  					frappe.chat.website.token().then(token => {
  						frappe.log.info(`Generated Guest Token - ${token}`)
  						frappe.store.set('guest_token', token)
  
  						setup_room(token).then(room => {
  							frappe.chatter.render({ room })
  						})
  					})
  				} else {
  					setup_room(token).then(room => {
  						frappe.chatter.render({ room })
  					})
  				}
  			} else {
  				frappe.chatter.render()
  			}
  		}
  	}
  }
  
  frappe.chat.setup  = () => {
  	frappe.log     = frappe.Logger.get('frappe.chat')
  
  	frappe.log.info('Setting up frappe.chat')
  	frappe.log.warn('TODO: frappe.chat.<object> requires a storage.')
  
  	if ( frappe.session.user !== 'Guest' ) {
  		// Create/Get Chat Profile for session User, retrieve enable_chat
  		frappe.log.info('Creating a Chat Profile.')
  
  		frappe.chat.profile.create('enable_chat').then(({ enable_chat }) => {
  			frappe.log.info(`Chat Profile created for User ${frappe.session.user}.`)
  
  			if ( 'desk' in frappe && frappe.sys_defaults ) { // same as desk?
  				const should_render = Boolean(parseInt(frappe.sys_defaults.enable_chat)) && enable_chat
  				frappe.chat.render(should_render)
  			}
  		})
  
  		// Triggered when a User updates his/her Chat Profile.
  		// Don't worry, enable_chat is broadcasted to this user only. No overhead. :)
  		frappe.chat.profile.on.update((user, profile) => {
  			if ( user === frappe.session.user && 'enable_chat' in profile ) {
  				frappe.log.warn(`Chat Profile update (Enable Chat - ${Boolean(profile.enable_chat)})`)
  				const should_render = Boolean(parseInt(frappe.sys_defaults.enable_chat)) && profile.enable_chat
  				frappe.chat.render(should_render)
  			}
  		})
  	} else {
  		// Website Settings
  		frappe.log.info('Retrieving Chat Website Settings.')
  		frappe.chat.website.settings(["socketio", "enable", "enable_from", "enable_to"])
  			.then(settings => {
  				frappe.log.info(`Chat Website Setting - ${JSON.stringify(settings)}`)
  				frappe.log.info(`Chat Website Setting - ${settings.enable ? "Enable" : "Disable"}`)
  
  				var should_render = settings.enable
  				if ( settings.enable_from && settings.enable_to ) {
  					frappe.log.info(`Enabling Chat Schedule - ${settings.enable_from.format()} : ${settings.enable_to.format()}`)
  
  					const range   = new frappe.datetime.range(settings.enable_from, settings.enable_to)
  					should_render = range.contains(frappe.datetime.now())
  				}
  
  				if ( should_render ) {
  					frappe.log.info("Initializing Socket.IO")
  					frappe.socketio.init(settings.socketio.port)
  				}
  
  				frappe.chat.render(should_render)
  		})
  	}
  }
  
  $(document).on('ready toolbar_setup', () =>
  {
  	frappe.chat.setup()
  })
  