@@ -6,7 +6,7 @@ import './share'; | |||||
import './review'; | import './review'; | ||||
import './document_follow'; | import './document_follow'; | ||||
import './user_image'; | import './user_image'; | ||||
import './form_viewers'; | |||||
import './form_sidebar_users'; | |||||
frappe.ui.form.Sidebar = Class.extend({ | frappe.ui.form.Sidebar = Class.extend({ | ||||
init: function(opts) { | init: function(opts) { | ||||
@@ -28,7 +28,7 @@ frappe.ui.form.Sidebar = Class.extend({ | |||||
this.make_attachments(); | this.make_attachments(); | ||||
this.make_review(); | this.make_review(); | ||||
this.make_shared(); | this.make_shared(); | ||||
this.make_viewers(); | |||||
this.make_sidebar_users(); | |||||
this.make_tags(); | this.make_tags(); | ||||
this.make_like(); | this.make_like(); | ||||
@@ -177,10 +177,10 @@ frappe.ui.form.Sidebar = Class.extend({ | |||||
parent: this.sidebar.find(".form-shared") | 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, | frm: this.frm, | ||||
parent: this.sidebar.find(".form-viewers") | |||||
$wrapper: this.sidebar, | |||||
}); | }); | ||||
}, | }, | ||||
add_user_action: function(label, click) { | add_user_action: function(label, click) { | ||||
@@ -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); | |||||
} | |||||
} |
@@ -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); | |||||
} | |||||
} |
@@ -78,6 +78,10 @@ | |||||
<li class="h6 viewers-label">{%= __("Currently Viewing") %}</li> | <li class="h6 viewers-label">{%= __("Currently Viewing") %}</li> | ||||
<li class="form-viewers"></li> | <li class="form-viewers"></li> | ||||
</ul> | </ul> | ||||
<ul class="list-unstyled sidebar-menu"> | |||||
<li class="h6 viewers-label">{%= __("Currently Typing") %}</li> | |||||
<li class="form-typers"></li> | |||||
</ul> | |||||
<ul class="list-unstyled sidebar-menu"> | <ul class="list-unstyled sidebar-menu"> | ||||
<a><li class="auto-repeat-status"><li></a> | <a><li class="auto-repeat-status"><li></a> | ||||
</ul> | </ul> | ||||
@@ -89,6 +89,14 @@ frappe.socketio = { | |||||
frappe.socketio.doc_close(frm.doctype, frm.docname); | 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() { | window.onbeforeunload = function() { | ||||
if (!cur_frm || cur_frm.is_new()) { | if (!cur_frm || cur_frm.is_new()) { | ||||
return; | return; | ||||
@@ -162,7 +170,14 @@ frappe.socketio = { | |||||
// notify that the user has closed this doc | // notify that the user has closed this doc | ||||
frappe.socketio.socket.emit('doc_close', doctype, docname); | 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() { | setup_listeners: function() { | ||||
frappe.socketio.socket.on('task_status_change', function(data) { | frappe.socketio.socket.on('task_status_change', function(data) { | ||||
frappe.socketio.process_response(data, data.status.toLowerCase()); | frappe.socketio.process_response(data, data.status.toLowerCase()); | ||||
@@ -66,6 +66,10 @@ frappe.views.CommunicationComposer = Class.extend({ | |||||
}) | }) | ||||
this.prepare(); | this.prepare(); | ||||
this.dialog.show(); | this.dialog.show(); | ||||
if (this.frm) { | |||||
$(document).trigger('form-typing', [this.frm]); | |||||
} | |||||
}, | }, | ||||
get_fields: function() { | get_fields: function() { | ||||
@@ -262,6 +266,10 @@ frappe.views.CommunicationComposer = Class.extend({ | |||||
subject: me.dialog.get_value("subject"), | subject: me.dialog.get_value("subject"), | ||||
content: me.dialog.get_value("content"), | content: me.dialog.get_value("content"), | ||||
}); | }); | ||||
if (me.frm) { | |||||
$(document).trigger("form-stopped-typing", [me.frm]); | |||||
} | |||||
} | } | ||||
this.dialog.on_page_show = function() { | this.dialog.on_page_show = function() { | ||||
@@ -37,7 +37,13 @@ frappe.views.FormFactory = class FormFactory extends frappe.views.Factory { | |||||
}); | }); | ||||
frappe.realtime.on("doc_viewers", function(data) { | 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'); | |||||
}); | }); | ||||
} | } | ||||
@@ -141,7 +141,6 @@ io.on('connection', function (socket) { | |||||
}); | }); | ||||
socket.on('doc_open', function (doctype, docname) { | socket.on('doc_open', function (doctype, docname) { | ||||
// show who is currently viewing the form | |||||
can_subscribe_doc({ | can_subscribe_doc({ | ||||
socket: socket, | socket: socket, | ||||
sid: sid, | sid: sid, | ||||
@@ -151,11 +150,25 @@ io.on('connection', function (socket) { | |||||
var room = get_open_doc_room(socket, doctype, docname); | var room = get_open_doc_room(socket, doctype, docname); | ||||
socket.join(room); | 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' | // remove this user from the list of 'who is currently viewing the form' | ||||
var room = get_open_doc_room(socket, doctype, docname); | var room = get_open_doc_room(socket, doctype, docname); | ||||
socket.leave(room); | 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) => { | 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; | 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) { | function get_user_room(socket, user) { | ||||
return get_site_name(socket) + ':user:' + 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)) { | if (!(args && args.doctype && args.docname)) { | ||||
return; | 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 | // 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 | // 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) { | if (clients.indexOf(s.id) !== -1) { | ||||
// this socket is connected to the room | // 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 | // notify | ||||
io.to(room).emit("doc_viewers", { | |||||
io.to(open_doc_room).emit(emit_event, { | |||||
doctype: args.doctype, | doctype: args.doctype, | ||||
docname: args.docname, | docname: args.docname, | ||||
viewers: viewers | |||||
users: users | |||||
}); | }); | ||||
} | |||||
} |