seitime-frappe/frappe/public/js/frappe/chat.js
Faris Ansari 92e8856588
New Build System: Rollup (#5010)
* JS build working

* Css build working!

* Uglify JS in production

* fix codacy

* Add frappe.commands.popen

* FIx ESLint errors

* Add socket.io to package.json

* ignore subprocess warnings

* Add babel-runtime

* sleep 20 after bench start

* remove set -e

* [FIX] non-shell subprocess call

* [FIX] use shell = False

* split commands
2018-02-20 13:47:48 +05:30

2412 lines
No EOL
60 KiB
JavaScript

// Frappe Chat
// Author - Achilles Rasquinha <achilles@frappe.io>
/**
* --------------------------------------------------------------------------------
* Developer Notes
* --------------------------------------------------------------------------------
*/
/* eslint semi: "never" */
// Fuck semicolons - https://mislav.net/2010/05/semicolons
// frappe extensions
// frappe.model extensions
frappe.provide('frappe.model')
/**
* @description Subscribe to a model for realtime updates.
*
* @example
* frappe.model.subscribe('User')
* // Subscribe to all User records
*
* frappe.model.subscribe('User', 'achilles@frappe.io')
* frappe.model.subscribe('User', ['achilles@frappe.io', 'rushabh@frappe.io'])
* // Subscribe to User of name(s)
*
* frappe.model.subscribe('User', 'achilles@frappe.io', 'username')
* frappe.model.subscribe('User', ['achilles@frappe.io', 'rushabh@frappe.io'], ['email', 'username'])
* // Subscribe to User of name for field(s)
*
* @todo Under Development
*/
frappe.model.subscribe = (doctype, name, field) =>
frappe.realtime.publish('frappe.model:subscribe', { doctype: doctype, name: name, field: field })
/**
* @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) {
if ( typeof moment === undefined )
throw new frappe.ImportError(`Moment.js not installed.`)
this.moment = instance ? moment(instance) : moment()
}
/**
* @description Returns a formatted string of the datetime object.
*/
format (format) {
const formatted = this.moment.format(format)
return formatted
}
}
/**
* @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._
// 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.ui extensions
frappe.provide('frappe.ui')
/**
* @description Frappe's Uploader Widget
*
* @see - Heavily inspired https://uppy.io
*
* @todo Under Development
*/
frappe.ui.Uploader = class {
constructor (wrapper, options = { }) {
this.options = frappe.ui.Uploader.OPTIONS
this.set_wrapper(wrapper)
this.set_options(options)
}
set_wrapper (wrapper) {
this.$wrapper = $(wrapper)
return this
}
set_options (options) {
this.options = { ...this.options, ...options }
return this
}
render ( ) {
const $template = $(frappe.ui.Uploader.TEMPLATE)
this.$wrapper.html($template)
}
}
frappe.ui.Uploader.Layout = { DIALOG: 'DIALOG' }
frappe.ui.Uploader.OPTIONS = {
layout: frappe.ui.Uploader.Layout.DIALOG
}
frappe.ui.Uploader.TEMPLATE =
`
<div class="foobar">
FooBar
</div>
`
frappe.provide('frappe.ui.keycode')
frappe.ui.keycode = { RETURN: 13 }
// frappe.stores - A registry for frappe stores.
frappe.provide('frappe.stores')
/**
* @description Frappe's Store Class
*
* @todo Under Development
*/
frappe.Store = class {
constructor ( ) {
}
}
// 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) {
if ( typeof name !== 'string' )
throw new frappe.TypeError(`Expected string for name, got ${typeof name} instead.`)
this.name = name
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) {
if ( !(name in frappe.loggers) )
frappe.loggers[name] = new frappe.Logger(name)
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.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, owner: 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) {
frappe.call("frappe.chat.doctype.chat_message.chat_message.send",
{ user: frappe.session.user, room: room, content: message })
}
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)
})
})
}
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 ( ) {
this.destroy()
const $wrapper = this.$wrapper
const options = this.options
const component = h(frappe.Chat.Widget, {
layout: options.layout,
target: options.target
})
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.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 ( room.type === "Group" || room.owner === frappe.session.user || room.last_message ) {
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 ( !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
this.make()
}
make ( ) {
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()
})
}
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>
<span class="indicator yellow"/> <b>${frappe.user.first_name(r.user)}</b>: ${r.content}
</span>
`
frappe.show_alert(alert, 3)
}
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:
[
{
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()
}
}
],
change: function (query) {
me.set_state({
query: query
})
}
})
const contacts = Object.keys(frappe.boot.user_info).map(key => {
return { owner: frappe.session.user, users: [frappe.boot.user_info[key].email] }
})
const rooms = state.query ? frappe.chat.room.search(state.query, state.rooms.concat(contacts)) : frappe.chat.room.sort(state.rooms)
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: props.layout, destroy: () => {
this.set_state({
room: { name: null, messages: [ ] }
})
}})
const component = props.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: "row" },
h("div", { class: "col-md-2 col-sm-3 layout-side-section" },
ActionBar, RoomList
),
h("div", { class: "col-md-10 col-sm-9 layout-main-section-wrapper" },
state.room.name ?
Room : (
h("div", "",
h("div", { class: "text-center text-muted" },
h(frappe.components.Octicon, { type: "comment-discussion", style: "font-size: 48px" }),
h("p","",__("Select a chat to start messaging."))
)
)
)
)
)
return component ?
h("div", { class: "frappe-chat" },
component
) : null
}
}
frappe.Chat.Widget.defaultState = {
query: "",
profile: { },
rooms: [ ],
room: { name: null, messages: [ ], typing: [ ] },
toggle: 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.toggle = this.toggle.bind(this)
this.state = frappe.Chat.Widget.Popper.defaultState
if ( props.target )
$(props.target).click(() => this.toggle())
}
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-popper', ({ currentTarget }) => {
if ( $(currentTarget).is('.page-container') )
this.toggle(false)
})
}
render ( ) {
const { props, state } = this
return !state.destroy ?
(
h("div", { class: "frappe-chat-popper" },
!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)
}
on_mounted ( )
{
$(document).ready(function (e)
{
// if ( e.keyCode === frappe.ui.keycode.CTRL )
})
}
render ( ) {
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.Chat.Widget.ActionBar.defaultState
= {
query: null
}
/**
* @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}`
)
)
}
}
/**
* @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 )
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 )
item.subtitle = props.last_message.content
}
if ( props.last_message )
item.timestamp = frappe.chat.pretty_datetime(props.last_message.creation)
return (
h("li", null,
h("a", { class: props.active ? "active": "", onclick: () => 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)
),
)
)
)
)
}
}
/**
* @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",
on_click: ( ) => {
const capture = new frappe.ui.Capture({
animate: false,
error: true
})
capture.show()
capture.submit(data_url => {
// data_url
})
}
}, {
icon: "file",
label: "File",
on_click: ( ) => {
}
}
])
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 panel-bg ${frappe._.is_mobile() ? "panel-span" : ""}` },
h(frappe.Chat.Widget.Room.Header, { ...props, on_back: props.destroy }),
// !frappe._.is_empty(props.messages) ?
h(frappe.chat.component.ChatList, {
messages: props.messages
}),
// :
// h("div", { class: "panel-body vcenter" },
// h("div","",
// 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: "chat-room-footer" },
h(frappe.chat.component.ChatForm, { actions: actions,
on_change: () => {
frappe.chat.message.typing(props.name)
},
on_submit: (message) => {
frappe.chat.message.send(props.name, message)
},
hint: hints
})
)
)
)
}
}
frappe.Chat.Widget.Room.Header
=
class extends Component {
render ( ) {
const { props } = this
const item = { }
if ( props.type === "Group" ) {
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.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 ?
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
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"},
props.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 {
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"}`},
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","",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.on_change = this.on_change.bind(this)
this.on_submit = this.on_submit.bind(this)
this.hint = this.hint.bind(this)
this.state = frappe.chat.component.ChatForm.defaultState
}
on_change (e) {
const { props, state } = this
const value = e.target.value
this.set_state({
[e.target.name]: value
})
props.on_change(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: [ ] })
}
}
on_submit (e) {
e.preventDefault()
if ( this.state.content ) {
this.props.on_submit(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.on_change, onsubmit: this.on_submit },
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: "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.on_click},
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.on_submit(e)
}
}),
h("div",{class:"input-group-btn"},
h(frappe.components.Button, { onclick: this.on_submit },
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)
}
})
}