@@ -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) { | |||
@@ -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="form-viewers"></li> | |||
</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"> | |||
<a><li class="auto-repeat-status"><li></a> | |||
</ul> | |||
@@ -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()); | |||
@@ -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() { | |||
@@ -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'); | |||
}); | |||
} | |||
@@ -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 | |||
}); | |||
} | |||
} |