diff --git a/frappe/public/js/frappe/form/sidebar/form_sidebar.js b/frappe/public/js/frappe/form/sidebar/form_sidebar.js index a145e47149..34b5fb3535 100644 --- a/frappe/public/js/frappe/form/sidebar/form_sidebar.js +++ b/frappe/public/js/frappe/form/sidebar/form_sidebar.js @@ -6,7 +6,7 @@ import './share'; import './review'; import './document_follow'; import './user_image'; -import './form_viewers'; +import './form_sidebar_users'; frappe.ui.form.Sidebar = Class.extend({ init: function(opts) { @@ -28,7 +28,7 @@ frappe.ui.form.Sidebar = Class.extend({ this.make_attachments(); this.make_review(); this.make_shared(); - this.make_viewers(); + this.make_sidebar_users(); this.make_tags(); this.make_like(); @@ -177,10 +177,10 @@ frappe.ui.form.Sidebar = Class.extend({ parent: this.sidebar.find(".form-shared") }); }, - make_viewers: function() { - this.frm.viewers = new frappe.ui.form.Viewers({ + make_sidebar_users: function() { + this.frm.viewers = new frappe.ui.form.SidebarUsers({ frm: this.frm, - parent: this.sidebar.find(".form-viewers") + $wrapper: this.sidebar, }); }, add_user_action: function(label, click) { diff --git a/frappe/public/js/frappe/form/sidebar/form_sidebar_users.js b/frappe/public/js/frappe/form/sidebar/form_sidebar_users.js new file mode 100644 index 0000000000..16f0f32376 --- /dev/null +++ b/frappe/public/js/frappe/form/sidebar/form_sidebar_users.js @@ -0,0 +1,87 @@ +frappe.ui.form.SidebarUsers = Class.extend({ + init: function(opts) { + $.extend(this, opts); + }, + get_users: function(type) { + let docinfo = this.frm.get_docinfo(); + return docinfo ? docinfo[type] || null: null; + }, + refresh: function(data_updated, type) { + this.parent = type == 'viewers'? this.$wrapper.find('.form-viewers'): this.$wrapper.find('.form-typers'); + this.parent.empty(); + + const users = this.get_users(type); + users && this.show_in_sidebar(users, type, data_updated); + }, + show_in_sidebar: function(users, type, show_alert) { + let sidebar_users = []; + let new_users = []; + let current_users = []; + + const message = type == 'viewers' ? 'viewing this document': 'typing...'; + + users.current.forEach(username => { + if (username === frappe.session.user) { + // current user + return; + } + + var user_info = frappe.user_info(username); + sidebar_users.push({ + image: user_info.image, + fullname: user_info.fullname, + abbr: user_info.abbr, + color: user_info.color, + title: __("{0} is currently {1}", [user_info.fullname, message]) + }); + + if (users.new.indexOf(username) !== -1) { + new_users.push(user_info.fullname); + } + + current_users.push(user_info.fullname); + }); + + if (sidebar_users.length) { + this.parent.parent().removeClass('hidden'); + this.parent.append(frappe.render_template('users_in_sidebar', {'users': sidebar_users})); + } else { + this.parent.parent().addClass('hidden'); + } + + // For typers always show the alert + // For viewers show the alert to new user viewing this document + const alert_users = type == 'viewers' ? new_users : current_users; + show_alert && this.show_alert(alert_users, message); + }, + show_alert(users, message) { + if (users.length) { + if (users.length===1) { + frappe.show_alert(__('{0} is currently {1}', [users[0], message])); + } else { + frappe.show_alert(__('{0} are currently {1}', [frappe.utils.comma_and(users), message])); + } + + } + } +}); + +frappe.ui.form.set_users = function(data, type) { + const doctype = data.doctype; + const docname = data.docname; + const docinfo = frappe.model.get_docinfo(doctype, docname); + + const past_users = ((docinfo && docinfo[type]) || {}).past || []; + const users = data.users || []; + const new_users = users.filter(user => !past_users.includes(user)); + + frappe.model.set_docinfo(doctype, docname, type, { + past: past_users.concat(new_users), + new: new_users, + current: users + }); + + if (cur_frm && cur_frm.doc && cur_frm.doc.doctype===doctype && cur_frm.doc.name==docname) { + cur_frm.viewers.refresh(true, type); + } +} \ No newline at end of file diff --git a/frappe/public/js/frappe/form/sidebar/form_viewers.js b/frappe/public/js/frappe/form/sidebar/form_viewers.js deleted file mode 100644 index 72f4984e94..0000000000 --- a/frappe/public/js/frappe/form/sidebar/form_viewers.js +++ /dev/null @@ -1,80 +0,0 @@ - - -frappe.ui.form.Viewers = Class.extend({ - init: function(opts) { - $.extend(this, opts); - }, - get_viewers: function() { - let docinfo = this.frm.get_docinfo(); - if (docinfo) { - return docinfo.viewers || {}; - } else { - return {}; - } - }, - refresh: function(data_updated) { - this.parent.empty(); - - var viewers = this.get_viewers(); - - var users = []; - var new_users = []; - for (var i=0, l=(viewers.current || []).length; i < l; i++) { - var username = viewers.current[i]; - if (username===frappe.session.user) { - // current user - continue; - } - - var user_info = frappe.user_info(username); - users.push({ - image: user_info.image, - fullname: user_info.fullname, - abbr: user_info.abbr, - color: user_info.color, - title: __("{0} is currently viewing this document", [user_info.fullname]) - }); - - if (viewers.new.indexOf(username)!==-1) { - new_users.push(user_info.fullname); - } - } - - if (users.length) { - this.parent.parent().removeClass("hidden"); - this.parent.append(frappe.render_template("users_in_sidebar", {"users": users})); - } else { - this.parent.parent().addClass("hidden"); - } - - if (data_updated && new_users.length) { - // new user viewing this document, who wasn't viewing in the past - if (new_users.length===1) { - frappe.show_alert(__("{0} is currently viewing this document", [new_users[0]])); - } else { - frappe.show_alert(__("{0} are currently viewing this document", [frappe.utils.comma_and(new_users)])); - } - - } - } -}); - -frappe.ui.form.set_viewers = function(data) { - var doctype = data.doctype; - var docname = data.docname; - var docinfo = frappe.model.get_docinfo(doctype, docname); - var past_viewers = ((docinfo && docinfo.viewers) || {}).past || []; - var viewers = data.viewers || []; - - var new_viewers = viewers.filter(viewer => !past_viewers.includes(viewer)); - - frappe.model.set_docinfo(doctype, docname, "viewers", { - past: past_viewers.concat(new_viewers), - new: new_viewers, - current: viewers - }); - - if (cur_frm && cur_frm.doc && cur_frm.doc.doctype===doctype && cur_frm.doc.name==docname) { - cur_frm.viewers.refresh(true); - } -} diff --git a/frappe/public/js/frappe/form/templates/form_sidebar.html b/frappe/public/js/frappe/form/templates/form_sidebar.html index c3f2de9c7e..77bff50461 100644 --- a/frappe/public/js/frappe/form/templates/form_sidebar.html +++ b/frappe/public/js/frappe/form/templates/form_sidebar.html @@ -78,6 +78,10 @@
  • {%= __("Currently Viewing") %}
  • + diff --git a/frappe/public/js/frappe/socketio_client.js b/frappe/public/js/frappe/socketio_client.js index 1411b6289d..52162f31c7 100644 --- a/frappe/public/js/frappe/socketio_client.js +++ b/frappe/public/js/frappe/socketio_client.js @@ -89,6 +89,14 @@ frappe.socketio = { frappe.socketio.doc_close(frm.doctype, frm.docname); }); + $(document).on('form-typing', function(e, frm) { + frappe.socketio.form_typing(frm.doctype, frm.docname); + }); + + $(document).on('form-stopped-typing', function(e, frm) { + frappe.socketio.form_stopped_typing(frm.doctype, frm.docname); + }); + window.onbeforeunload = function() { if (!cur_frm || cur_frm.is_new()) { return; @@ -162,7 +170,14 @@ frappe.socketio = { // notify that the user has closed this doc frappe.socketio.socket.emit('doc_close', doctype, docname); }, - + form_typing: function(doctype, docname) { + // notifiy that the user is typing on the doc + frappe.socketio.socket.emit('doc_typing', doctype, docname); + }, + form_stopped_typing: function(doctype, docname) { + // notifiy that the user has stopped typing + frappe.socketio.socket.emit('doc_typing_stopped', doctype, docname); + }, setup_listeners: function() { frappe.socketio.socket.on('task_status_change', function(data) { frappe.socketio.process_response(data, data.status.toLowerCase()); diff --git a/frappe/public/js/frappe/views/communication.js b/frappe/public/js/frappe/views/communication.js index ba290417f5..8dad5d9121 100755 --- a/frappe/public/js/frappe/views/communication.js +++ b/frappe/public/js/frappe/views/communication.js @@ -66,6 +66,10 @@ frappe.views.CommunicationComposer = Class.extend({ }) this.prepare(); this.dialog.show(); + + if (this.frm) { + $(document).trigger('form-typing', [this.frm]); + } }, get_fields: function() { @@ -262,6 +266,10 @@ frappe.views.CommunicationComposer = Class.extend({ subject: me.dialog.get_value("subject"), content: me.dialog.get_value("content"), }); + + if (me.frm) { + $(document).trigger("form-stopped-typing", [me.frm]); + } } this.dialog.on_page_show = function() { diff --git a/frappe/public/js/frappe/views/formview.js b/frappe/public/js/frappe/views/formview.js index 033569a03c..7440ab198d 100644 --- a/frappe/public/js/frappe/views/formview.js +++ b/frappe/public/js/frappe/views/formview.js @@ -37,7 +37,13 @@ frappe.views.FormFactory = class FormFactory extends frappe.views.Factory { }); frappe.realtime.on("doc_viewers", function(data) { - frappe.ui.form.set_viewers(data); + // set users that currently viewing the form + frappe.ui.form.set_users(data, 'viewers'); + }); + + frappe.realtime.on("doc_typers", function(data) { + // set users that currently typing on the form + frappe.ui.form.set_users(data, 'typers'); }); } diff --git a/socketio.js b/socketio.js index 95e8414914..d93579928e 100644 --- a/socketio.js +++ b/socketio.js @@ -141,7 +141,6 @@ io.on('connection', function (socket) { }); socket.on('doc_open', function (doctype, docname) { - // show who is currently viewing the form can_subscribe_doc({ socket: socket, sid: sid, @@ -151,11 +150,25 @@ io.on('connection', function (socket) { var room = get_open_doc_room(socket, doctype, docname); socket.join(room); - send_viewers({ - socket: socket, - doctype: doctype, - docname: docname, - }); + // show who is currently viewing the form + send_users( + { + socket: socket, + doctype: doctype, + docname: docname, + }, + 'view' + ); + + // show who is currently typing on the form + send_users( + { + socket: socket, + doctype: doctype, + docname: docname, + }, + 'type' + ) } }); }); @@ -164,11 +177,44 @@ io.on('connection', function (socket) { // remove this user from the list of 'who is currently viewing the form' var room = get_open_doc_room(socket, doctype, docname); socket.leave(room); - send_viewers({ - socket: socket, - doctype: doctype, - docname: docname, - }); + send_users( + { + socket: socket, + doctype: doctype, + docname: docname, + }, + 'view' + ); + }); + + socket.on('doc_typing', function (doctype, docname) { + // show users that are currently typing on the form + const room = get_typing_room(socket, doctype, docname); + socket.join(room); + + send_users( + { + socket: socket, + doctype: doctype, + docname: docname, + }, + 'type' + ); + }); + + socket.on('doc_typing_stopped', function (doctype, docname) { + // remove this user from the list of users currently typing on the form' + const room = get_typing_room(socket, doctype, docname); + socket.leave(room); + + send_users( + { + socket: socket, + doctype: doctype, + docname: docname, + }, + 'type' + ); }); socket.on('upload-accept-slice', (data) => { @@ -246,6 +292,10 @@ function get_open_doc_room(socket, doctype, docname) { return get_site_name(socket) + ':open_doc:' + doctype + '/' + docname; } +function get_typing_room(socket, doctype, docname) { + return get_site_name(socket) + ':typing:' + doctype + '/' + docname; +} + function get_user_room(socket, user) { return get_site_name(socket) + ':user:' + user; } @@ -325,36 +375,38 @@ function can_subscribe_doc(args) { }); } -function send_viewers(args) { - // send to doc room, 'users currently viewing this document' + +function send_users(args, action) { if (!(args && args.doctype && args.docname)) { return; } - // open doc room - var room = get_open_doc_room(args.socket, args.doctype, args.docname); + const open_doc_room = get_open_doc_room(args.socket, args.doctype, args.docname); - var socketio_room = io.sockets.adapter.rooms[room] || {}; + const room = action == 'view' ? open_doc_room: get_typing_room(args.socket, args.doctype, args.docname); + const socketio_room = io.sockets.adapter.rooms[room] || {}; // for compatibility with both v1.3.7 and 1.4.4 - var clients_dict = ("sockets" in socketio_room) ? socketio_room.sockets : socketio_room; + const clients_dict = ('sockets' in socketio_room) ? socketio_room.sockets : socketio_room; // socket ids connected to this room - var clients = Object.keys(clients_dict || {}); + const clients = Object.keys(clients_dict || {}); - var viewers = []; - for (var i in io.sockets.sockets) { - var s = io.sockets.sockets[i]; + let users = []; + for (let i in io.sockets.sockets) { + const s = io.sockets.sockets[i]; if (clients.indexOf(s.id) !== -1) { // this socket is connected to the room - viewers.push(s.user); + users.push(s.user); } } + const emit_event = action == 'view' ? 'doc_viewers' : 'doc_typers'; + // notify - io.to(room).emit("doc_viewers", { + io.to(open_doc_room).emit(emit_event, { doctype: args.doctype, docname: args.docname, - viewers: viewers + users: users }); -} \ No newline at end of file +}