[IMP] im: implemented group chat

bzr revid: nicolas.vanhoren@openerp.com-20130903083648-8xer767nsbi8fapr
This commit is contained in:
niv-openerp 2013-09-03 10:36:48 +02:00
commit b183c50f3a
7 changed files with 168 additions and 66 deletions

View File

@ -169,7 +169,7 @@ class im_message(osv.osv):
# how fun it is to always need to reorder results from read # how fun it is to always need to reorder results from read
mess_ids = self.search(cr, openerp.SUPERUSER_ID, ["&", ['id', '>', last], "|", ['from_id', '=', my_id], ['to_id', 'in', [my_id]]], order="id", context=context) mess_ids = self.search(cr, openerp.SUPERUSER_ID, ["&", ['id', '>', last], "|", ['from_id', '=', my_id], ['to_id', 'in', [my_id]]], order="id", context=context)
mess = self.read(cr, openerp.SUPERUSER_ID, mess_ids, ["id", "message", "from_id", "session_id", "date"], context=context) mess = self.read(cr, openerp.SUPERUSER_ID, mess_ids, ["id", "message", "from_id", "session_id", "date", "technical"], context=context)
index = {} index = {}
for i in xrange(len(mess)): for i in xrange(len(mess)):
index[mess[i]["id"]] = mess[i] index[mess[i]["id"]] = mess[i]
@ -182,12 +182,13 @@ class im_message(osv.osv):
users_status = users.read(cr, openerp.SUPERUSER_ID, users_watch, ["im_status"], context=context) users_status = users.read(cr, openerp.SUPERUSER_ID, users_watch, ["im_status"], context=context)
return {"res": mess, "last": last, "dbname": cr.dbname, "users_status": users_status} return {"res": mess, "last": last, "dbname": cr.dbname, "users_status": users_status}
def post(self, cr, uid, message, to_session_id, uuid=None, context=None): def post(self, cr, uid, message, to_session_id, technical=False, uuid=None, context=None):
assert_uuid(uuid) assert_uuid(uuid)
my_id = self.pool.get('im.user').get_my_id(cr, uid, uuid) my_id = self.pool.get('im.user').get_my_id(cr, uid, uuid)
session = self.pool.get('im.session').browse(cr, uid, to_session_id, context) session = self.pool.get('im.session').browse(cr, uid, to_session_id, context)
to_ids = [x.id for x in session.user_ids if x.id != my_id] to_ids = [x.id for x in session.user_ids if x.id != my_id]
self.create(cr, openerp.SUPERUSER_ID, {"message": message, 'from_id': my_id, 'to_id': [(6, 0, to_ids)], 'session_id': to_session_id}, context=context) self.create(cr, openerp.SUPERUSER_ID, {"message": message, 'from_id': my_id,
'to_id': [(6, 0, to_ids)], 'session_id': to_session_id, 'technical': technical}, context=context)
notify_channel(cr, "im_channel", {'type': 'message', 'receivers': [my_id] + to_ids}) notify_channel(cr, "im_channel", {'type': 'message', 'receivers': [my_id] + to_ids})
return False return False
@ -206,22 +207,31 @@ class im_session(osv.osv):
} }
# Todo: reuse existing sessions if possible # Todo: reuse existing sessions if possible
def session_get(self, cr, uid, user_to, uuid=None, context=None): def session_get(self, cr, uid, users_to, uuid=None, context=None):
my_id = self.pool.get("im.user").get_my_id(cr, uid, uuid, context=context) my_id = self.pool.get("im.user").get_my_id(cr, uid, uuid, context=context)
users = [my_id] + users_to
domain = []
for user_to in users:
domain.append(('user_ids', 'in', [user_to]))
sids = self.search(cr, openerp.SUPERUSER_ID, domain, context=context, limit=1)
session_id = None session_id = None
if user_to: for session in self.browse(cr, uid, sids, context=context):
# FP Note: does the ORM allows something better than this? == on many2many if len(session.user_ids) == len(users):
sids = self.search(cr, openerp.SUPERUSER_ID, [('user_ids', 'in', [user_to]), ('user_ids', 'in', [my_id])], context=context, limit=1) session_id = session.id
for session in self.browse(cr, uid, sids, context=context): break
if len(session.user_ids) == 2:
session_id = session.id
break
if not session_id: if not session_id:
session_id = self.create(cr, openerp.SUPERUSER_ID, { session_id = self.create(cr, openerp.SUPERUSER_ID, {
'user_ids': [(6, 0, [user_to, my_id])] 'user_ids': [(6, 0, users)]
}, context=context) }, context=context)
return self.read(cr, uid, session_id, context=context) return self.read(cr, uid, session_id, context=context)
def add_to_session(self, cr, uid, session_id, user_id, uuid=None, context=None):
my_id = self.pool.get("im.user").get_my_id(cr, uid, uuid, context=context)
session = self.read(cr, uid, session_id, context=context)
if my_id not in session.get("user_ids"):
raise Exception("Not allowed to modify a session when you are not in it.")
self.write(cr, uid, session_id, {"user_ids": [(4, user_id)]}, context=context)
class im_user(osv.osv): class im_user(osv.osv):
_name = "im.user" _name = "im.user"

View File

@ -183,7 +183,6 @@
} }
.oe_im_chatview_online { .oe_im_chatview_online {
display: none;
margin-top: -4px; margin-top: -4px;
width: 11px; width: 11px;
height: 11px; height: 11px;

View File

@ -18,7 +18,8 @@
im_common.notification = function(message) { im_common.notification = function(message) {
instance.client.do_warn(message); instance.client.do_warn(message);
}; };
im_common.connection = openerp.session; // TODO: allow to use a different host for the chat
im_common.connection = new openerp.Session(self, null, {session_id: openerp.session.session_id});
var im = new instance.im.InstantMessaging(self); var im = new instance.im.InstantMessaging(self);
im.appendTo(instance.client.$el); im.appendTo(instance.client.$el);
@ -54,6 +55,7 @@
this.set("current_search", ""); this.set("current_search", "");
this.users = []; this.users = [];
this.c_manager = new im_common.ConversationManager(this); this.c_manager = new im_common.ConversationManager(this);
window.im_conversation_manager = this.c_manager;
this.on("change:right_offset", this.c_manager, _.bind(function() { this.on("change:right_offset", this.c_manager, _.bind(function() {
this.c_manager.set("right_offset", this.get("right_offset")); this.c_manager.set("right_offset", this.get("right_offset"));
}, this)); }, this));
@ -70,7 +72,15 @@
var self = this; var self = this;
return this.c_manager.start_polling(); return this.c_manager.start_polling().then(function() {
self.c_manager.on("new_conversation", self, function(conv) {
conv.$el.droppable({
drop: function(event, ui) {
self.add_user(conv, ui.draggable.data("user"));
}
});
});
});
}, },
calc_box: function() { calc_box: function() {
var $topbar = instance.client.$(".oe_topbar"); var $topbar = instance.client.$(".oe_topbar");
@ -97,7 +107,7 @@
_.each(users, function(user) { _.each(users, function(user) {
var widget = new instance.im.UserWidget(self, self.c_manager.get_user(user.id)); var widget = new instance.im.UserWidget(self, self.c_manager.get_user(user.id));
widget.appendTo(self.$(".oe_im_users")); widget.appendTo(self.$(".oe_im_users"));
widget.on("activate_user", self, self.activate_user); widget.on("activate_user", self, function(user) {self.c_manager.chat_with_users([user]);});
self.users.push(widget); self.users.push(widget);
}); });
_.each(old_users, function(user) { _.each(old_users, function(user) {
@ -127,11 +137,8 @@
} }
this.shown = ! this.shown; this.shown = ! this.shown;
}, },
activate_user: function(user) { add_user: function(conversation, user) {
var self = this; conversation.add_user(user);
im_common.connection.model("im.session").call("session_get", [user.get("id"), self.c_manager.me.get("uuid")]).then(function(session) {
self.c_manager.activate_session(session.id, true);
});
}, },
}); });
@ -146,6 +153,8 @@
this.user.add_watcher(); this.user.add_watcher();
}, },
start: function() { start: function() {
this.$el.data("user", this.user);
this.$el.draggable({helper: "clone"});
var change_status = function() { var change_status = function() {
this.$(".oe_im_user_online").toggle(this.user.get("im_status") === true); this.$(".oe_im_user_online").toggle(this.user.get("im_status") === true);
}; };
@ -161,4 +170,8 @@
}, },
}); });
im_common.technical_messages_handlers.force_kitten = function() {
openerp.webclient.to_kitten();
};
})(); })();

View File

@ -141,12 +141,16 @@ function declare($, _, openerp) {
no_cache[el] = el; no_cache[el] = el;
}, this); }, this);
var self = this; var self = this;
var def;
if (_.size(no_cache) === 0) if (_.size(no_cache) === 0)
return $.when(); def = $.when();
else else
return im_common.connection.model("im.user").call("read", [_.values(no_cache), []]).then(function(users) { def = im_common.connection.model("im.user").call("read", [_.values(no_cache), []]).then(function(users) {
self.add_to_user_cache(users); self.add_to_user_cache(users);
}); });
return def.then(function() {
return _.map(user_ids, function(id) { return self.get_user(id); });
});
}, },
add_to_user_cache: function(user_recs) { add_to_user_cache: function(user_recs) {
_.each(user_recs, function(user_rec) { _.each(user_recs, function(user_rec) {
@ -211,6 +215,21 @@ function declare($, _, openerp) {
openerp.webclient.set_title_part("im_messages", this.get("waiting_messages") === 0 ? undefined : openerp.webclient.set_title_part("im_messages", this.get("waiting_messages") === 0 ? undefined :
_.str.sprintf(_t("%d Messages"), this.get("waiting_messages"))); _.str.sprintf(_t("%d Messages"), this.get("waiting_messages")));
}, },
chat_with_users: function(users) {
var self = this;
return im_common.connection.model("im.session").call("session_get", [_.map(users, function(user) {return user.get("id");}),
self.me.get("uuid")]).then(function(session) {
return self.activate_session(session.id, true);
});
},
chat_with_all_users: function() {
var self = this;
return im_common.connection.model("im.user").call("search", [[["uuid", "=", false]]]).then(function(user_ids) {
return self.ensure_users(_.without(user_ids, self.me.get("id")));
}).then(function(users) {
return self.chat_with_users(users);
});
},
activate_session: function(session_id, focus) { activate_session: function(session_id, focus) {
var conv = _.find(this.conversations, function(conv) {return conv.session_id == session_id;}); var conv = _.find(this.conversations, function(conv) {return conv.session_id == session_id;});
var def = $.when(); var def = $.when();
@ -223,6 +242,7 @@ function declare($, _, openerp) {
}); });
this.conversations.push(conv); this.conversations.push(conv);
this.calc_positions(); this.calc_positions();
this.trigger("new_conversation", conv);
}, this)); }, this));
} }
if (focus) { if (focus) {
@ -241,9 +261,14 @@ function declare($, _, openerp) {
} }
var defs = []; var defs = [];
_.each(messages, function(message) { _.each(messages, function(message) {
defs.push(self.activate_session(message.session_id[0]).then(function(conv) { if (! message.technical) {
return conv.received_message(message); defs.push(self.activate_session(message.session_id[0]).then(function(conv) {
})); return conv.received_message(message);
}));
} else {
var json = JSON.parse(message.message);
defs.push($.when(im_common.technical_messages_handlers[json.type](self, message)));
}
}); });
return $.when.apply($, defs); return $.when.apply($, defs);
}, },
@ -266,7 +291,7 @@ function declare($, _, openerp) {
im_common.Conversation = openerp.Widget.extend({ im_common.Conversation = openerp.Widget.extend({
className: "openerp_style oe_im_chatview", className: "openerp_style oe_im_chatview",
events: { events: {
"keydown input": "send_message", "keydown input": "keydown",
"click .oe_im_chatview_close": "destroy", "click .oe_im_chatview_close": "destroy",
"click .oe_im_chatview_header": "show_hide" "click .oe_im_chatview_header": "show_hide"
}, },
@ -280,41 +305,70 @@ function declare($, _, openerp) {
this.shown = true; this.shown = true;
this.set("pending", 0); this.set("pending", 0);
this.inputPlaceholder = this.options.defaultInputPlaceholder; this.inputPlaceholder = this.options.defaultInputPlaceholder;
this.users = []; this.set("users", []);
this.set("disconnected", false);
this.others = []; this.others = [];
}, },
start: function() { start: function() {
var self = this; var self = this;
self.$().append(openerp.qweb.render("im_common.conversation", {widget: self}));
this.$().hide();
var change_status = function() {
var disconnected = _.every(this.get("users"), function(u) { return u.get("im_status") === false; });
self.set("disconnected", disconnected);
this.$(".oe_im_chatview_users").html(openerp.qweb.render("im_common.conversation.header",
{widget: self, to_url: _.bind(im_common.connection.url, im_common.connection)}));
};
this.on("change:users", this, function(unused, ev) {
_.each(ev.oldValue, function(user) {
user.off("change:im_status", self, change_status);
});
_.each(ev.newValue, function(user) {
user.on("change:im_status", self, change_status);
});
change_status.call(self);
_.each(ev.oldValue, function(user) {
if (! _.contains(ev.newValue, user)) {
user.remove_watcher();
}
});
_.each(ev.newValue, function(user) {
if (! _.contains(ev.oldValue, user)) {
user.add_watcher();
}
});
});
this.on("change:disconnected", this, function() {
self.$().toggleClass("oe_im_chatview_disconnected_status", this.get("disconnected"));
self._go_bottom();
});
self.on("change:right_position", self, self.calc_pos);
self.on("change:bottom_position", self, self.calc_pos);
self.full_height = self.$().height();
self.calc_pos();
self.on("change:pending", self, _.bind(function() {
if (self.get("pending") === 0) {
self.$(".oe_im_chatview_nbr_messages").text("");
} else {
self.$(".oe_im_chatview_nbr_messages").text("(" + self.get("pending") + ")");
}
}, self));
return this.refresh_users().then(function() {
self.$().show();
});
},
refresh_users: function() {
var self = this;
var user_ids; var user_ids;
return im_common.connection.model("im.session").call("read", [self.session_id]).then(function(session) { return im_common.connection.model("im.session").call("read", [self.session_id]).then(function(session) {
user_ids = _.without(session.user_ids, self.c_manager.me.get("id")); user_ids = _.without(session.user_ids, self.c_manager.me.get("id"));
return self.c_manager.ensure_users(session.user_ids); return self.c_manager.ensure_users(user_ids);
}).then(function() { }).then(function(users) {
self.users = _.map(user_ids, function(id) {return self.c_manager.get_user(id);}); self.set("users", users);
_.each(self.users, function(user) {
user.add_watcher();
});
// TODO: correctly display status
self.$().append(openerp.qweb.render("im_common.conversation", {widget: self, to_url: _.bind(im_common.connection.url, im_common.connection)}));
var change_status = function() {
self.$().toggleClass("oe_im_chatview_disconnected_status", self.users[0].get("im_status") === false);
self.$(".oe_im_chatview_online").toggle(self.users[0].get("im_status") === true);
self._go_bottom();
};
self.users[0].on("change:im_status", self, change_status);
change_status.call(self);
self.on("change:right_position", self, self.calc_pos);
self.on("change:bottom_position", self, self.calc_pos);
self.full_height = self.$().height();
self.calc_pos();
self.on("change:pending", self, _.bind(function() {
if (self.get("pending") === 0) {
self.$(".oe_im_chatview_nbr_messages").text("");
} else {
self.$(".oe_im_chatview_nbr_messages").text("(" + self.get("pending") + ")");
}
}, self));
}); });
}, },
show_hide: function() { show_hide: function() {
@ -342,16 +396,16 @@ function declare($, _, openerp) {
} else { } else {
this.set("pending", this.get("pending") + 1); this.set("pending", this.get("pending") + 1);
} }
this.c_manager.ensure_users([message.from_id[0]]).then(_.bind(function() { this.c_manager.ensure_users([message.from_id[0]]).then(_.bind(function(users) {
var user = this.c_manager.get_user(message.from_id[0]); var user = users[0];
if (! _.contains(this.users, user) && ! _.contains(this.others, user)) { if (! _.contains(this.get("users"), user) && ! _.contains(this.others, user)) {
this.others.push(user); this.others.push(user);
user.add_watcher(); user.add_watcher();
} }
this._add_bubble(user, message.message, openerp.str_to_datetime(message.date)); this._add_bubble(user, message.message, openerp.str_to_datetime(message.date));
}, this)); }, this));
}, },
send_message: function(e) { keydown: function(e) {
if(e && e.which !== 13) { if(e && e.which !== 13) {
return; return;
} }
@ -360,9 +414,13 @@ function declare($, _, openerp) {
return; return;
} }
this.$("input").val(""); this.$("input").val("");
this.send_message(mes);
},
send_message: function(message, technical) {
technical = technical || false;
var send_it = _.bind(function() { var send_it = _.bind(function() {
var model = im_common.connection.model("im.message"); var model = im_common.connection.model("im.message");
return model.call("post", [mes, this.session_id], {uuid: this.c_manager.me.get("uuid"), context: {}}); return model.call("post", [message, this.session_id, technical], {uuid: this.c_manager.me.get("uuid"), context: {}});
}, this); }, this);
var tries = 0; var tries = 0;
send_it().then(_.bind(function() {}, function(error, e) { send_it().then(_.bind(function() {}, function(error, e) {
@ -393,13 +451,21 @@ function declare($, _, openerp) {
_go_bottom: function() { _go_bottom: function() {
this.$(".oe_im_chatview_content").scrollTop($(this.$(".oe_im_chatview_content").children()[0]).height()); this.$(".oe_im_chatview_content").scrollTop($(this.$(".oe_im_chatview_content").children()[0]).height());
}, },
add_user: function(user) {
if (user === this.me || _.contains(this.get("users"), user))
return;
im_common.connection.model("im.session").call("add_to_session",
[this.session_id, user.get("id"), this.c_manager.me.get("uuid")]).then(_.bind(function() {
this.send_message(JSON.stringify({"type": "session_modified"}), true);
}, this));
},
focus: function() { focus: function() {
this.$(".oe_im_chatview_input").focus(); this.$(".oe_im_chatview_input").focus();
if (! this.shown) if (! this.shown)
this.show_hide(); this.show_hide();
}, },
destroy: function() { destroy: function() {
_.each(this.users, function(user) { _.each(this.get("users"), function(user) {
user.remove_watcher(); user.remove_watcher();
}) })
_.each(this.others, function(user) { _.each(this.others, function(user) {
@ -410,6 +476,14 @@ function declare($, _, openerp) {
} }
}); });
im_common.technical_messages_handlers = {};
im_common.technical_messages_handlers.session_modified = function(c_manager, message) {
c_manager.activate_session(message.session_id[0], true).then(function(conv) {
conv.refresh_users();
});
};
return im_common; return im_common;
} }

View File

@ -3,13 +3,12 @@
<templates> <templates>
<t t-name="im_common.conversation"> <t t-name="im_common.conversation">
<div class="oe_im_chatview_header"> <div class="oe_im_chatview_header">
<img t-att-src="to_url('/im/static/src/img/green.png')" class="oe_im_chatview_online"/> <span class="oe_im_chatview_users"/>
<t t-esc="widget.users[0].get('name')"/>
<scan class="oe_im_chatview_nbr_messages" /> <scan class="oe_im_chatview_nbr_messages" />
<button class="oe_im_chatview_close">×</button> <button class="oe_im_chatview_close">×</button>
</div> </div>
<div class="oe_im_chatview_disconnected"> <div class="oe_im_chatview_disconnected">
<t t-esc='widget.users[0].get("name") + _t(" is offline. He/She will receive your messages on his/her next connection.")'/> All users are offline. They will receive your messages on their next connection.
</div> </div>
<div class="oe_im_chatview_content"> <div class="oe_im_chatview_content">
<div></div> <div></div>
@ -19,6 +18,13 @@
</div> </div>
</t> </t>
<t t-name="im_common.conversation.header">
<span t-foreach="widget.get('users')" t-as="user">
<img t-if="user.get('im_status')" t-att-src="to_url('/im/static/src/img/green.png')" class="oe_im_chatview_online"/>
<t t-esc="user.get('name')"/>
</span>
</t>
<t t-name="im_common.conversation_bubble"> <t t-name="im_common.conversation_bubble">
<div class="oe_im_chatview_bubble"> <div class="oe_im_chatview_bubble">
<div class="oe_im_chatview_clip"> <div class="oe_im_chatview_clip">

View File

@ -175,7 +175,7 @@ class im_livechat_channel(osv.osv):
if len(users) == 0: if len(users) == 0:
return False return False
user_id = random.choice(users).id user_id = random.choice(users).id
session = self.pool.get("im.session").session_get(cr, uid, user_id, uuid, context=context) session = self.pool.get("im.session").session_get(cr, uid, [user_id], uuid, context=context)
self.pool.get("im.session").write(cr, openerp.SUPERUSER_ID, session.get("id"), {'channel_id': channel_id}, context=context) self.pool.get("im.session").write(cr, openerp.SUPERUSER_ID, session.get("id"), {'channel_id': channel_id}, context=context)
return session.get("id") return session.get("id")

View File

@ -110,7 +110,7 @@ define(["openerp", "im_common", "underscore", "require", "jquery",
conv.received_message({ conv.received_message({
message: self.options.defaultMessage, message: self.options.defaultMessage,
date: openerp.datetime_to_str(new Date()), date: openerp.datetime_to_str(new Date()),
from_id: [conv.users[0].get("id"), "Unknown"] from_id: [conv.get("users")[0].get("id"), "Unknown"]
}); });
} }
}); });