diff --git a/frappe/public/css/chat.css b/frappe/public/css/chat.css index 8e198daa55..607faee3e9 100644 --- a/frappe/public/css/chat.css +++ b/frappe/public/css/chat.css @@ -380,7 +380,7 @@ a.no-decoration:active { resize: none; padding-left: 0px; padding-right: 0px; - overflow: auto; + overflow: hidden; } .chat-room-footer .chat-form .fa { font-size: 14px; diff --git a/frappe/public/js/frappe/chat.js b/frappe/public/js/frappe/chat.js index 5e9b28eaee..9024221ae4 100644 --- a/frappe/public/js/frappe/chat.js +++ b/frappe/public/js/frappe/chat.js @@ -32,7 +32,7 @@ frappe.provide('frappe.model') * @todo Under Development */ frappe.model.subscribe = (doctype, name, field) => - frappe.realtime.publish('frappe.model:subscribe', { doctype: doctype, name: name, field: field }) + frappe.realtime.publish('frappe.model:subscribe', { doctype: doctype, name: name, field: field }) /** * @description The base class for all Frappe Errors. @@ -48,49 +48,49 @@ frappe.model.subscribe = (doctype, name, field) => * @todo Requires "transform-builtin-extend" for Babel 6 */ frappe.Error = class extends Error { - constructor (message) { - super (message) + constructor (message) { + super (message) - this.name = 'FrappeError' + this.name = 'FrappeError' - if ( typeof Error.captureStackTrace === 'function' ) - Error.captureStackTrace(this, this.constructor) - else - this.stack = (new Error(message)).stack - } + if ( typeof Error.captureStackTrace === 'function' ) + Error.captureStackTrace(this, this.constructor) + else + this.stack = (new Error(message)).stack + } } /** * @description TypeError */ frappe.TypeError = class extends frappe.Error { - constructor (message) { - super (message) + constructor (message) { + super (message) - this.name = this.constructor.name - } + this.name = this.constructor.name + } } /** * @description ValueError */ frappe.ValueError = class extends frappe.Error { - constructor (message) { - super (message) + constructor (message) { + super (message) - this.name = this.constructor.name - } + this.name = this.constructor.name + } } /** * @description ImportError */ frappe.ImportError = class extends frappe.Error { - constructor (message) { - super (message) + constructor (message) { + super (message) - this.name = this.constructor.name - } + this.name = this.constructor.name + } } // frappe.datetime @@ -103,23 +103,23 @@ frappe.provide('frappe.datetime') * 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.`) + /** + * @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() - } + 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 a formatted string of the datetime object. + */ + format (format) { + const formatted = this.moment.format(format) + return formatted + } } /** @@ -131,12 +131,12 @@ frappe.datetime.datetime = class { frappe.datetime.now = () => new frappe.datetime.datetime() frappe.datetime.equal = (a, b, type) => { - a = a.moment - b = b.moment + a = a.moment + b = b.moment - const equal = a.isSame(b, type) + const equal = a.isSame(b, type) - return equal + return equal } /** @@ -156,16 +156,16 @@ frappe.datetime.equal = (a, b, type) => { * // returns 1 */ frappe.datetime.compare = (a, b) => { - a = a.moment - b = b.moment + a = a.moment + b = b.moment - if ( a.isBefore(b) ) - return 1 - else - if ( b.isBefore(a) ) - return -1 - else - return 0 + if ( a.isBefore(b) ) + return 1 + else + if ( b.isBefore(a) ) + return -1 + else + return 0 } // frappe._ @@ -187,10 +187,10 @@ frappe.provide('frappe._') * // returns "bar foo" */ frappe._.format = (string, object) => { - for (const key in object) - string = string.replace(`{${key}}`, object[key]) + for (const key in object) + string = string.replace(`{${key}}`, object[key]) - return string + return string } /** @@ -209,20 +209,20 @@ frappe._.format = (string, object) => { * @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 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) + const fuse = new Fuse(dataset, options) + const result = fuse.search(query) - return result + return result } /** @@ -287,10 +287,10 @@ frappe._.head = arr => frappe._.is_empty(arr) ? undefined : arr[0] * @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.`) + if ( Array.isArray(array) ) + return array.slice() + else + throw frappe.TypeError(`Expected Array, recieved ${typeof array} instead.`) } /** @@ -315,18 +315,18 @@ frappe._.copy_array = array => { * @todo Handle other cases. */ frappe._.is_empty = value => { - let empty = false + 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 + 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 + return empty } /** @@ -371,11 +371,11 @@ frappe._.squash = list => Array.isArray(list) && list.length === 1 ? list[0] : l * @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) + 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 + return mobile } /** @@ -417,37 +417,37 @@ frappe.provide('frappe.ui') * @todo Under Development */ frappe.ui.Uploader = class { - constructor (wrapper, options = { }) { - this.options = frappe.ui.Uploader.OPTIONS - this.set_wrapper(wrapper) - this.set_options(options) - } + constructor (wrapper, options = { }) { + this.options = frappe.ui.Uploader.OPTIONS + this.set_wrapper(wrapper) + this.set_options(options) + } - set_wrapper (wrapper) { - this.$wrapper = $(wrapper) + set_wrapper (wrapper) { + this.$wrapper = $(wrapper) - return this - } + return this + } - set_options (options) { - this.options = { ...this.options, ...options } + set_options (options) { + this.options = { ...this.options, ...options } - return this - } + return this + } - render ( ) { - const $template = $(frappe.ui.Uploader.TEMPLATE) - this.$wrapper.html($template) - } + 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 + layout: frappe.ui.Uploader.Layout.DIALOG } frappe.ui.Uploader.TEMPLATE = `
` @@ -462,9 +462,9 @@ frappe.provide('frappe.stores') * @todo Under Development */ frappe.Store = class { - constructor ( ) { + constructor ( ) { - } + } } // frappe.loggers - A registry for frappe loggers. @@ -480,50 +480,50 @@ frappe.provide('frappe.loggers') * // 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.`) + /** + * @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 - } + 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] - } + /** + * @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) } + 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() + 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) - } - } + 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' } @@ -556,23 +556,23 @@ frappe.provide('frappe.chat.profile') * 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) + 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) - }) - }) + 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) + }) + }) } /** @@ -585,16 +585,16 @@ frappe.chat.profile.create = (fields, fn) => { * 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) - }) - }) + 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 @@ -614,23 +614,23 @@ frappe.provide('frappe.chat.profile.on') * }) */ frappe.chat.profile.on.update = function (fn) { - frappe.realtime.on("frappe.chat.profile:update", r => fn(r.user, r.data)) + 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" - } + name: "Online", + color: "green" + }, { + name: "Away", + color: "yellow" + }, { + name: "Busy", + color: "red" + }, { + name: "Offline", + color: "darkgrey" + } ] // frappe.chat.room @@ -656,26 +656,26 @@ frappe.provide('frappe.chat.room') * }) */ frappe.chat.room.create = function (kind, owner, users, name, fn) { - if ( typeof name === "function" ) { - fn = name - name = null - } + 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) } + 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) + if ( fn ) + fn(room) - resolve(room) - }) - }) + resolve(room) + }) + }) } /** @@ -708,48 +708,48 @@ frappe.chat.room.create = function (kind, owner, users, name, fn) { * }) */ 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 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) - } + if ( typeof fields === "function" ) { + fn = fields + fields = null + } + else + if ( typeof fields === "string" ) + fields = frappe._.as_array(fields) + } - return new Promise(resolve => { + 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 = [ ] + 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) + if ( fn ) + fn(rooms) - resolve(rooms) - }) - }) + resolve(rooms) + }) + }) } /** @@ -761,7 +761,7 @@ frappe.chat.room.get = function (names, fields, fn) { * frappe.chat.room.subscribe("CR00001") */ frappe.chat.room.subscribe = function (rooms) { - frappe.realtime.publish("frappe.chat.room:subscribe", rooms) + frappe.realtime.publish("frappe.chat.room:subscribe", rooms) } /** @@ -778,19 +778,19 @@ frappe.chat.room.subscribe = function (rooms) { * }) */ 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) } }) + 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) - }) - }) + if ( fn ) + fn(messages) + + resolve(messages) + }) + }) } /** @@ -802,19 +802,19 @@ frappe.chat.room.history = function (name, fn) { * @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]) + 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 + return rooms } /** @@ -824,21 +824,21 @@ frappe.chat.room.search = function (query, rooms) { * @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) + 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 + return rooms } // frappe.chat.room.on @@ -850,13 +850,13 @@ frappe.provide('frappe.chat.room.on') * @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) - }) + 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) + }) } /** @@ -865,7 +865,7 @@ frappe.chat.room.on.update = function (fn) { * @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) })) + frappe.realtime.on("frappe.chat.room:create", r => fn({ ...r, creation: new frappe.datetime.datetime(r.creation) })) } /** @@ -874,70 +874,70 @@ frappe.chat.room.on.create = function (fn) { * @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.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.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.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) + 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) - }) - }) + resolve(response.message) + }) + }) } frappe.chat.message.sort = (messages) => { - if ( !frappe._.is_empty(messages) ) - messages.sort((a, b) => frappe.datetime.compare(b.creation, a.creation)) + if ( !frappe._.is_empty(messages) ) + messages.sort((a, b) => frappe.datetime.compare(b.creation, a.creation)) - return messages + 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.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.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.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") + 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 @@ -952,45 +952,45 @@ frappe.provide('frappe.chat.sound') * frappe.chat.sound.play("message") */ frappe.chat.sound.play = function (name, volume = 0.1) { - // frappe._.play_sound(`chat-${name}`) - const $audio = $(``) - $audio.attr('volume', volume) + // frappe._.play_sound(`chat-${name}`) + const $audio = $(``) + $audio.attr('volume', volume) - if ( frappe._.is_empty($audio) ) - $(document).append($audio) + if ( frappe._.is_empty($audio) ) + $(document).append($audio) - if ( !$audio.paused ) { - frappe.log.info('Stopping sound playing.') - $audio[0].pause() - $audio.attr('currentTime', 0) - } + 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.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) + 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) + } + 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) - }) - }) + resolve(frappe.chat.emojis) + }) + }) } const { h, Component } = hyper @@ -1010,30 +1010,30 @@ frappe.provide('frappe.chat.component') frappe.components.Button = class extends Component { - render ( ) { - const { props } = this - const size = frappe.components.Button.SIZE[props.size] + 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 - ) - ) - } + 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" - } + small: { + class: "btn-sm" + }, + large: { + class: "btn-lg" + } } frappe.components.Button.defaultProps = { - type: "default", - block: false + type: "default", + block: false } /** @@ -1044,29 +1044,29 @@ frappe.components.Button.defaultProps 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 }) - ) - ) - } + 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" + icon: "octicon octicon-plus" } frappe.components.FAB.SIZE = { - small: { - class: "frappe-fab-sm" - }, - large: { - class: "frappe-fab-lg" - } + small: { + class: "frappe-fab-sm" + }, + large: { + class: "frappe-fab-lg" + } } /** @@ -1077,11 +1077,11 @@ frappe.components.FAB.SIZE frappe.components.Indicator = class extends Component { - render ( ) { - const { props } = this + render ( ) { + const { props } = this - return props.color ? h("span", { ...props, class: `indicator ${props.color}` }) : null - } + return props.color ? h("span", { ...props, class: `indicator ${props.color}` }) : null + } } /** @@ -1090,15 +1090,15 @@ class extends Component { frappe.components.FontAwesome = class extends Component { - render ( ) { - const { props } = this + render ( ) { + const { props } = this - return props.type ? h("i", { ...props, class: `fa ${props.fixed ? "fa-fw" : ""} fa-${props.type} ${props.class}` }) : null - } + return props.type ? h("i", { ...props, class: `fa ${props.fixed ? "fa-fw" : ""} fa-${props.type} ${props.class}` }) : null + } } frappe.components.FontAwesome.defaultProps = { - fixed: false + fixed: false } /** @@ -1109,11 +1109,11 @@ frappe.components.FontAwesome.defaultProps frappe.components.Octicon = class extends Component { - render ( ) { - const { props } = this + render ( ) { + const { props } = this - return props.type ? h("i", { ...props, class: `octicon octicon-${props.type}` }) : null - } + return props.type ? h("i", { ...props, class: `octicon octicon-${props.type}` }) : null + } } /** @@ -1127,32 +1127,32 @@ class extends Component { 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 + 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) - ) - ) - } + 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" - } + small: { + class: "avatar-small" + }, + large: { + class: "avatar-large" + }, + medium: { + class: "avatar-medium" + } } /** @@ -1172,108 +1172,108 @@ frappe.components.Avatar.SIZE 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 - } + /** + * @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.options = frappe.Chat.OPTIONS - this.set_wrapper(selector ? selector : "body") - this.set_options(options) + this.set_wrapper(selector ? selector : "body") + this.set_options(options) - // Load Emojis. - frappe.chat.emoji() - } + // 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) + /** + * 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 - } + 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 } + /** + * 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 - } + 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") + /** + * @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 - } + 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() + /** + * @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 $wrapper = this.$wrapper + const options = this.options - const component = h(frappe.Chat.Widget, { - layout: options.layout, - target: options.target - }) + const component = h(frappe.Chat.Widget, { + layout: options.layout, + target: options.target + }) - hyper.render(component, $wrapper[0]) + hyper.render(component, $wrapper[0]) - return this - } + return this + } } frappe.Chat.Layout = { - PAGE: "page", POPPER: "popper" + PAGE: "page", POPPER: "popper" } frappe.Chat.OPTIONS = { - layout: frappe.Chat.Layout.POPPER + layout: frappe.Chat.Layout.POPPER } /** @@ -1282,323 +1282,323 @@ frappe.Chat.OPTIONS frappe.Chat.Widget = class extends Component { - constructor (props) { - super (props) + 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 = [ ] + 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) - } + 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) - } + 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, ...update } + } - return r - }) + return r + }) - if ( !exists ) - frappe.chat.room.get(room, (room) => this.room.add(room)) - else - this.set_state({ rooms }) + 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) - } + 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 } + 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.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() - } + this.make() + } - make ( ) { - frappe.chat.profile.create([ - "status", "message_preview", "notification_tones", "conversation_tones" - ]).then(profile => { - this.set_state({ profile }) + 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)}.`) + 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) - }) + 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}.`) + 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 = ` ${frappe.user.full_name(user)} is currently ${update.status}` - frappe.show_alert(alert, 3) - } - } - }) + 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 = ` ${frappe.user.full_name(user)} is currently ${update.status}` + 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.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.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') + 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 - ` - - ${frappe.user.first_name(r.user)}: ${r.content} - - ` - - 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 } }) - } - }) + 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 + ` + + ${frappe.user.first_name(r.user)}: ${r.content} + + ` + + 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.`) - }) - } + 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: - [ { - type: "primary", - 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" + 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() + 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("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 - }) - } - }) + 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 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-extra-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 - } + 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-extra-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 + query: "", + profile: { }, + rooms: [ ], + room: { name: null, messages: [ ], typing: [ ] }, + toggle: false } frappe.Chat.Widget.defaultProps = { - layout: frappe.Chat.Layout.POPPER + layout: frappe.Chat.Layout.POPPER } /** @@ -1607,69 +1607,69 @@ frappe.Chat.Widget.defaultProps = { frappe.Chat.Widget.Popper = class extends Component { - constructor (props) { - super (props) + constructor (props) { + super (props) - this.toggle = this.toggle.bind(this) + this.toggle = this.toggle.bind(this) - this.state = frappe.Chat.Widget.Popper.defaultState + this.state = frappe.Chat.Widget.Popper.defaultState - if ( props.target ) - $(props.target).click(() => this.toggle()) - } + 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 }) + 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) - } + this.props.toggle(toggle) + } - on_mounted ( ) { - $(document.body).on('click', '.page-container, .frappe-chat-popper', ({ currentTarget }) => { - if ( $(currentTarget).is('.page-container') ) - this.toggle(false) - }) - } + 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 - } + 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 + active: false, + destroy: false } /** @@ -1678,51 +1678,51 @@ frappe.Chat.Widget.Popper.defaultState frappe.Chat.Widget.ActionBar = class extends Component { - constructor (props) { - super (props) - - this.change = this.change.bind(this) - this.submit = this.submit.bind(this) + constructor (props) { + super (props) + + this.change = this.change.bind(this) + this.submit = this.submit.bind(this) - this.state = frappe.Chat.Widget.ActionBar.defaultState - } + this.state = frappe.Chat.Widget.ActionBar.defaultState + } - change (e) { - const { props, state } = this + change (e) { + const { props, state } = this - this.set_state({ - [e.target.name]: e.target.value - }) + this.set_state({ + [e.target.name]: e.target.value + }) - props.change(state.query) - } + props.change(state.query) + } - submit (e) { - const { props, state } = this - - e.preventDefault() + submit (e) { + const { props, state } = this + + e.preventDefault() - props.submit(state.query) - } + props.submit(state.query) + } - render ( ) { - const { props, state } = this - const { actions } = props + 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 - ) - ) - } + 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 + query: null } /** @@ -1731,16 +1731,16 @@ frappe.Chat.Widget.ActionBar.defaultState frappe.Chat.Widget.ActionBar.Action = class extends Component { - render ( ) { - const { props } = this + 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}` - ) - ) - } + return ( + h(frappe.components.Button, { size: "small", class: "btn-action", ...props }, + props.icon ? h("i", { class: props.icon }) : null, + `${props.icon ? " " : ""}${props.label}` + ) + ) + } } /** @@ -1749,16 +1749,16 @@ class extends Component { frappe.Chat.Widget.RoomList = class extends Component { - render ( ) { - const { props } = this - const rooms = props.rooms + 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 - } + 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 + } } /** @@ -1767,53 +1767,53 @@ class extends Component { frappe.Chat.Widget.RoomList.Item = class extends Component { - render ( ) { - const { props } = this - const item = { } + render ( ) { + const { props } = this + const item = { } - if ( props.type === "Group" ) { - item.title = props.room_name - item.image = props.avatar + 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 + 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) + 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 ( !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) + 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) - ), - ) - ) - ) - ) - } + 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) + ), + ) + ) + ) + ) + } } /** @@ -1822,36 +1822,36 @@ class extends 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 - }) - ) - ) + 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 - ) - ) - } + 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" } + left: { class: "media-left" }, right: { class: "media-right" } } /** @@ -1860,179 +1860,179 @@ frappe.Chat.Widget.MediaProfile.POSITION 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) + 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(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" - }) - ) - } - } - ] + 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() + 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: ( ) => { - - } - } - ]) + 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 - } + 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 ${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 - }) - ) - ) - ) - } + return ( + h("div", { class: `panel panel-default ${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 + render ( ) { + const { props } = this - const item = { } - - if ( props.type === "Group" ) { - item.route = `Form/Chat Room/${props.name}` + const item = { } + + if ( props.type === "Group" ) { + item.route = `Form/Chat Room/${props.name}` - item.title = props.room_name - item.image = props.avatar + 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 + 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.route = `Form/User/${user}` - item.title = frappe.user.full_name(user) - item.image = frappe.user.image(user) + item.title = frappe.user.full_name(user) + item.image = frappe.user.image(user) - if ( !frappe._.is_empty(props.typing) ) - item.subtitle = 'typing...' - } + 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" }, - 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" }, + const popper = props.layout === frappe.Chat.Layout.POPPER || frappe._.is_mobile() + + return ( + h("div", { class: "panel-heading" }, + 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" }, - ) - ) - ) - ) - ) - } + ) + ) + ) + ) + ) + } } /** @@ -2043,33 +2043,33 @@ class extends Component { frappe.chat.component.ChatList = class extends Component { - on_mounted ( ) { - this.$element = $('.frappe-chat').find('.chat-list') - this.$element.scrollTop(this.$element[0].scrollHeight) - } + 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) - } + 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 + 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') }) + 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 !frappe._.is_empty(messages) ? ( - h("div",{class:"chat-list list-group"}, - messages.map(m => h(frappe.chat.component.ChatList.Item, {...m})) - ) - ) : null - } + messages.push(message) + } + + return !frappe._.is_empty(messages) ? ( + h("div",{class:"chat-list list-group"}, + messages.map(m => h(frappe.chat.component.ChatList.Item, {...m})) + ) + ) : null + } } /** @@ -2087,31 +2087,31 @@ class extends Component { frappe.chat.component.ChatList.Item = class extends Component { - render ( ) { - const { props } = this + render ( ) { + const { props } = this - const me = props.user === frappe.session.user + 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" : ""}`}, - !me && !props.groupable && !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) - ) - ) - ) - } + 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" : ""}`}, + !me && !props.groupable && !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) + ) + ) + ) + } } /** @@ -2129,37 +2129,37 @@ class extends Component { frappe.chat.component.ChatBubble = class extends Component { - render ( ) { - const { props } = this + 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 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 + 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 - ) - ) - ) - } + 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 + ) + ) + ) + } } /** @@ -2168,138 +2168,138 @@ class extends 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) + 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.hint = this.hint.bind(this) - this.state = frappe.chat.component.ChatForm.defaultState - } + this.state = frappe.chat.component.ChatForm.defaultState + } - on_change (e) { - const { props, state } = this - const value = e.target.value + on_change (e) { + const { props, state } = this + const value = e.target.value - this.set_state({ - [e.target.name]: value - }) + this.set_state({ + [e.target.name]: value + }) - props.on_change(state) + props.on_change(state) - this.hint(value) - } + this.hint(value) + } - hint (value) { - const { props, state } = this + hint (value) { + const { props, state } = this - if ( props.hint ) { - const tokens = value.split(" ") - const sliced = tokens.slice(0, tokens.length - 1) + if ( props.hint ) { + const tokens = value.split(" ") + const sliced = tokens.slice(0, tokens.length - 1) - const token = tokens[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 } + 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: [ ] }) - } - } + 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() + on_submit (e) { + e.preventDefault() - if ( this.state.content ) { - this.props.on_submit(this.state.content) + if ( this.state.content ) { + this.props.on_submit(this.state.content) - this.set_state({ content: null }) - } - } + this.set_state({ content: null }) + } + } - render ( ) { - const { props, state } = this + 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 }) - ), - ) - ) - ) - ) - ) - } + 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: [ ], + content: null, + hints: [ ], } @@ -2311,33 +2311,73 @@ frappe.chat.component.ChatForm.defaultState frappe.chat.component.EmojiPicker = class extends Component { - render ( ) { - const { props } = this + 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) - ) - ) - ) - ) - } + 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 + render ( ) { + const { props } = this - return ( - h("div", { class: "list-group" }, - - ) - ) - } + return ( + h("div", { class: "list-group" }, + + ) + ) + } } + +/** + * @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) + } + }) +} \ No newline at end of file diff --git a/frappe/public/less/chat.less b/frappe/public/less/chat.less index d7a6a6f5e1..5fb6602daa 100644 --- a/frappe/public/less/chat.less +++ b/frappe/public/less/chat.less @@ -261,7 +261,7 @@ resize: none; padding-left: 0px; padding-right: 0px; - overflow: auto; + overflow: hidden; } .fa