details: https://code.tryton.org/tryton/commit/b4deb9f4d972 branch: default user: Cédric Krier <[email protected]> date: Tue Dec 02 13:08:05 2025 +0100 description: Allow users to subscribe to chats diffstat:
sao/CHANGELOG | 1 + sao/images/tryton-notification-off.svg | 1 + sao/images/tryton-notification-on.svg | 1 + sao/src/chat.js | 53 +++++++++++ sao/src/common.js | 2 + tryton/CHANGELOG | 1 + tryton/tryton/chat.py | 44 +++++++++ tryton/tryton/data/pixmaps/tryton/tryton-notification-off.svg | 1 + tryton/tryton/data/pixmaps/tryton/tryton-notification-on.svg | 1 + tryton/tryton/rpc.py | 11 +- trytond/trytond/ir/chat.py | 36 ++++++- trytond/trytond/ir/ui/menu.py | 2 + 12 files changed, 145 insertions(+), 9 deletions(-) diffs (315 lines): diff -r 481aed0501da -r b4deb9f4d972 sao/CHANGELOG --- a/sao/CHANGELOG Tue Dec 02 18:11:10 2025 +0100 +++ b/sao/CHANGELOG Tue Dec 02 13:08:05 2025 +0100 @@ -1,3 +1,4 @@ +* Allow users to subscribe to chats * Manage user notification * Add search on empty relation field to domain parser * Escape completion content with custom format (issue14363) diff -r 481aed0501da -r b4deb9f4d972 sao/images/tryton-notification-off.svg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sao/images/tryton-notification-off.svg Tue Dec 02 13:08:05 2025 +0100 @@ -0,0 +1,1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M160-200v-80h80v-280q0-33 8.5-65t25.5-61l60 60q-7 16-10.5 32.5T320-560v280h248L56-792l56-56 736 736-56 56-146-144H160Zm560-154-80-80v-126q0-66-47-113t-113-47q-26 0-50 8t-44 24l-58-58q20-16 43-28t49-18v-28q0-25 17.5-42.5T480-880q25 0 42.5 17.5T540-820v28q80 20 130 84.5T720-560v206Zm-276-50Zm36 324q-33 0-56.5-23.5T400-160h160q0 33-23.5 56.5T480-80Zm33-481Z"/></svg> \ No newline at end of file diff -r 481aed0501da -r b4deb9f4d972 sao/images/tryton-notification-on.svg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sao/images/tryton-notification-on.svg Tue Dec 02 13:08:05 2025 +0100 @@ -0,0 +1,1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M80-560q0-100 44.5-183.5T244-882l47 64q-60 44-95.5 111T160-560H80Zm720 0q0-80-35.5-147T669-818l47-64q75 55 119.5 138.5T880-560h-80ZM160-200v-80h80v-280q0-83 50-147.5T420-792v-28q0-25 17.5-42.5T480-880q25 0 42.5 17.5T540-820v28q80 20 130 84.5T720-560v280h80v80H160Zm320-300Zm0 420q-33 0-56.5-23.5T400-160h160q0 33-23.5 56.5T480-80ZM320-280h320v-280q0-66-47-113t-113-47q-66 0-113 47t-47 113v280Z"/></svg> \ No newline at end of file diff -r 481aed0501da -r b4deb9f4d972 sao/src/chat.js --- a/sao/src/chat.js Tue Dec 02 18:11:10 2025 +0100 +++ b/sao/src/chat.js Tue Dec 02 13:08:05 2025 +0100 @@ -50,6 +50,59 @@ 'class': 'chat', }); + let btn_group = jQuery('<div/>', { + 'class': 'btn-group', + 'role': 'group', + }).appendTo(el); + + let subscribe_btn = jQuery('<button/>', { + 'class': 'btn btn-default pull-right', + 'type': 'button', + 'title': Sao.i18n.gettext("Toggle notification"), + }).append(Sao.common.ICONFACTORY.get_icon_img( + 'tryton-notification')) + .appendTo(btn_group); + + Sao.rpc({ + 'method': 'model.ir.chat.channel.get_followers', + 'params': [this.record, {}], + }, Sao.Session.current_session).then((followers) => { + set_subscribe_state(~followers.users.indexOf( + Sao.Session.current_session.login)); + }) + + let set_subscribe_state = (subscribed) => { + let img; + if (subscribed) { + img = 'tryton-notification-on'; + subscribe_btn.addClass('active').off().click(unsubscribe); + } else { + img = 'tryton-notification-off'; + subscribe_btn.removeClass('active').off().click(subscribe); + } + subscribe_btn.html(Sao.common.ICONFACTORY.get_icon_img(img)); + } + + let subscribe = () => { + let session = Sao.Session.current_session; + Sao.rpc({ + 'method': 'model.ir.chat.channel.subscribe', + 'params': [this.record, {}], + }, session).then(() => { + set_subscribe_state(true); + }) + }; + + let unsubscribe = () => { + let session = Sao.Session.current_session; + Sao.rpc({ + 'method': 'model.ir.chat.channel.unsubscribe', + 'params': [this.record, {}], + }, session).then(() => { + set_subscribe_state(false); + }) + }; + this._messages = jQuery('<div/>', { 'class': 'chat-messages', }).appendTo(jQuery('<div/>', { diff -r 481aed0501da -r b4deb9f4d972 sao/src/common.js --- a/sao/src/common.js Tue Dec 02 18:11:10 2025 +0100 +++ b/sao/src/common.js Tue Dec 02 13:08:05 2025 +0100 @@ -3155,6 +3155,8 @@ 'tryton-menu', 'tryton-note', 'tryton-notification', + 'tryton-notification-off', + 'tryton-notification-on', 'tryton-ok', 'tryton-open', 'tryton-print', diff -r 481aed0501da -r b4deb9f4d972 tryton/CHANGELOG --- a/tryton/CHANGELOG Tue Dec 02 18:11:10 2025 +0100 +++ b/tryton/CHANGELOG Tue Dec 02 13:08:05 2025 +0100 @@ -1,3 +1,4 @@ +* Allow users to subscribe to chats * Manage user notification * Add search on empty relation field to domain parser * Add support for multiple button in the tree view diff -r 481aed0501da -r b4deb9f4d972 tryton/tryton/chat.py --- a/tryton/tryton/chat.py Tue Dec 02 18:11:10 2025 +0100 +++ b/tryton/tryton/chat.py Tue Dec 02 13:08:05 2025 +0100 @@ -84,9 +84,53 @@ 'size-allocate', scroll_to_bottom) def __build(self): + tooltips = common.Tooltips() + widget = Gtk.VBox() widget.set_spacing(3) + hbuttonbox = Gtk.HButtonBox() + hbuttonbox.set_layout(Gtk.ButtonBoxStyle.END) + widget.pack_start(hbuttonbox, expand=False, fill=True, padding=0) + + subscribe_btn = Gtk.ToggleButton() + subscribe_btn.set_image(common.IconFactory.get_image( + 'tryton-notification', Gtk.IconSize.SMALL_TOOLBAR)) + tooltips.set_tip(subscribe_btn, _("Toggle notification")) + subscribe_btn.set_relief(Gtk.ReliefStyle.NONE) + hbuttonbox.pack_start( + subscribe_btn, expand=False, fill=True, padding=0) + hbuttonbox.set_child_non_homogeneous(subscribe_btn, True) + + followers = rpc.execute( + 'model', 'ir.chat.channel', 'get_followers', self.record, + rpc.CONTEXT) + + def set_subscribe_state(subscribed): + if subscribed: + img = 'tryton-notification-on' + else: + img = 'tryton-notification-off' + subscribe_btn.set_image(common.IconFactory.get_image( + img, Gtk.IconSize.SMALL_TOOLBAR)) + + subscribed = rpc._LOGIN in followers['users'] + set_subscribe_state(subscribed) + subscribe_btn.set_active(subscribed) + + def toggle_subscribe(button): + if button.props.active: + rpc.execute( + 'model', 'ir.chat.channel', 'subscribe', self.record, + rpc.CONTEXT) + else: + rpc.execute( + 'model', 'ir.chat.channel', 'unsubscribe', self.record, + rpc.CONTEXT) + set_subscribe_state(button.props.active) + + subscribe_btn.connect('toggled', toggle_subscribe) + def _submit(button): buffer = input_.get_buffer() self.send_message( diff -r 481aed0501da -r b4deb9f4d972 tryton/tryton/data/pixmaps/tryton/tryton-notification-off.svg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tryton/tryton/data/pixmaps/tryton/tryton-notification-off.svg Tue Dec 02 13:08:05 2025 +0100 @@ -0,0 +1,1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M160-200v-80h80v-280q0-33 8.5-65t25.5-61l60 60q-7 16-10.5 32.5T320-560v280h248L56-792l56-56 736 736-56 56-146-144H160Zm560-154-80-80v-126q0-66-47-113t-113-47q-26 0-50 8t-44 24l-58-58q20-16 43-28t49-18v-28q0-25 17.5-42.5T480-880q25 0 42.5 17.5T540-820v28q80 20 130 84.5T720-560v206Zm-276-50Zm36 324q-33 0-56.5-23.5T400-160h160q0 33-23.5 56.5T480-80Zm33-481Z"/></svg> \ No newline at end of file diff -r 481aed0501da -r b4deb9f4d972 tryton/tryton/data/pixmaps/tryton/tryton-notification-on.svg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tryton/tryton/data/pixmaps/tryton/tryton-notification-on.svg Tue Dec 02 13:08:05 2025 +0100 @@ -0,0 +1,1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M80-560q0-100 44.5-183.5T244-882l47 64q-60 44-95.5 111T160-560H80Zm720 0q0-80-35.5-147T669-818l47-64q75 55 119.5 138.5T880-560h-80ZM160-200v-80h80v-280q0-83 50-147.5T420-792v-28q0-25 17.5-42.5T480-880q25 0 42.5 17.5T540-820v28q80 20 130 84.5T720-560v280h80v80H160Zm320-300Zm0 420q-33 0-56.5-23.5T400-160h160q0 33-23.5 56.5T480-80ZM320-280h320v-280q0-66-47-113t-113-47q-66 0-113 47t-47 113v280Z"/></svg> \ No newline at end of file diff -r 481aed0501da -r b4deb9f4d972 tryton/tryton/rpc.py --- a/tryton/tryton/rpc.py Tue Dec 02 18:11:10 2025 +0100 +++ b/tryton/tryton/rpc.py Tue Dec 02 13:08:05 2025 +0100 @@ -20,6 +20,7 @@ logger = logging.getLogger(__name__) CONNECTION = None _USER = None +_LOGIN = None CONTEXT = {} _VIEW_CACHE = {} _TOOLBAR_CACHE = {} @@ -86,7 +87,7 @@ def set_service_session(parameters): from tryton import common from tryton.bus import Bus - global CONNECTION, _USER + global CONNECTION, _USER, _LOGIN host = CONFIG['login.host'] hostname = common.get_hostname(host) port = common.get_port(host) @@ -102,6 +103,7 @@ if _USER != renew_id: raise ValueError _USER = user_id + _LOGIN = username bus_url_host = parameters.get('bus_url_host', [''])[0] session = ':'.join(map(str, [username, user_id, session])) if CONNECTION is not None: @@ -114,7 +116,7 @@ def login(parameters): from tryton import common from tryton.bus import Bus - global CONNECTION, _USER + global CONNECTION, _USER, _LOGIN host = CONFIG['login.host'] hostname = common.get_hostname(host) port = common.get_port(host) @@ -127,6 +129,7 @@ result = connection.common.db.login(username, parameters, language) logger.debug('%r', result) _USER = result[0] + _LOGIN = username session = ':'.join(map(str, [username] + result[:2])) bus_url_host = result[2] if CONNECTION is not None: @@ -138,7 +141,7 @@ def logout(): - global CONNECTION, _USER + global CONNECTION, _USER, _LOGIN if CONNECTION is not None: try: logger.info('common.db.logout()') @@ -148,7 +151,7 @@ pass CONNECTION.close() CONNECTION = None - _USER = None + _USER = _LOGIN = None def reset_password(): diff -r 481aed0501da -r b4deb9f4d972 trytond/trytond/ir/chat.py --- a/trytond/trytond/ir/chat.py Tue Dec 02 18:11:10 2025 +0100 +++ b/trytond/trytond/ir/chat.py Tue Dec 02 13:08:05 2025 +0100 @@ -44,6 +44,7 @@ unsubscribe=RPC(readonly=False), subscribe_email=RPC(readonly=False), unsubscribe_email=RPC(readonly=False), + get_followers=RPC(), post=RPC(readonly=False, result=int), get_models=RPC(), get=RPC(), @@ -73,19 +74,24 @@ ]) if channels: channel, = channels - else: + elif not Transaction().readonly: channel = cls(resource=str(resource)) channel.save() + else: + return return channel @classmethod - def subscribe(cls, resource, username): + def subscribe(cls, resource, username=None): pool = Pool() Follower = pool.get('ir.chat.follower') User = pool.get('res.user') - user, = User.search([ - ('login', '=', username), - ]) + if username is not None: + user, = User.search([ + ('login', '=', username), + ]) + else: + user = User(Transaction().user) channel = cls._get_channel(resource) Follower.add_user(channel, user) @@ -113,6 +119,26 @@ Follower.remove_email(channel, email) @classmethod + def get_followers(cls, resource): + pool = Pool() + Follower = pool.get('ir.chat.follower') + users, emails = [], [] + channel = cls._get_channel(resource) + if channel: + followers = Follower.search([ + ('channel', '=', channel), + ]) + for follower in followers: + if follower.user: + users.append(follower.user.login) + elif follower.email: + emails.append(follower.email) + return { + 'users': users, + 'emails': emails, + } + + @classmethod def post(cls, resource, content, audience='internal'): pool = Pool() Message = pool.get('ir.chat.message') diff -r 481aed0501da -r b4deb9f4d972 trytond/trytond/ir/ui/menu.py --- a/trytond/trytond/ir/ui/menu.py Tue Dec 02 18:11:10 2025 +0100 +++ b/trytond/trytond/ir/ui/menu.py Tue Dec 02 13:08:05 2025 +0100 @@ -61,6 +61,8 @@ 'tryton-menu', 'tryton-note', 'tryton-notification', + 'tryton-notification-off', + 'tryton-notification-on', 'tryton-ok', 'tryton-open', 'tryton-print',
