Philipp Hörist pushed to branch chat_markers at gajim / gajim
Commits: 0cba9f53 by Daniel Brötzmann at 2019-09-29T19:42:30Z XML Console: Make accounts selectable
- - - - - 5db3dc55 by Philipp Hörist at 2019-10-01T18:08:37Z Fix LMC in when ChatControl is not open
When the ChatControl is not open we create a ChatEvent. The current message id was not passed to ChatEvent. So on correction the ConversationTextview could not find the messages it was supposed to correct
This adds a message_id attr to ChatEvent.
- - - - - be5c65de by Yann Leboulanger at 2019-10-01T19:45:09Z start implementing XEP-0333, chat markers
- - - - - b3c67112 by Yann Leboulanger at 2019-10-01T19:45:09Z fix error
- - - - - 4032986b by Philipp Hörist at 2019-10-01T19:45:09Z Remove chat marker from modules
- - - - - 9c17266f by Yann Leboulanger at 2019-10-01T19:45:09Z add missing chat_markers module
- - - - - 12bc8165 by Yann Leboulanger at 2019-10-01T19:45:09Z Revert "Remove chat marker from modules"
This reverts commit f5377d98af23483244d71196a8e5cfab96111118.
- - - - - f28a7812 by Philipp Hörist at 2019-10-01T21:36:38Z Adapt to nbxmpp changes
- Move displayed event handler from roster to gui interface - Rename msg_id to message_id
- - - - -
17 changed files:
- gajim/app_actions.py - gajim/application.py - gajim/chat_control.py - gajim/chat_control_base.py - gajim/common/app.py - gajim/common/connection.py - gajim/common/events.py - gajim/common/modules/__init__.py - + gajim/common/modules/chat_markers.py - gajim/common/modules/message.py - gajim/data/gui/application_menu.ui - gajim/data/gui/xml_console_window.ui → gajim/data/gui/xml_console.ui - gajim/groupchat_control.py - gajim/gtk/xml_console.py - gajim/gui_interface.py - gajim/gui_menu_builder.py - gajim/session.py
Changes:
===================================== gajim/app_actions.py ===================================== @@ -249,12 +249,8 @@ def on_server_info(action, param):
def on_xml_console(action, param): - account = param.get_string() - window = app.get_app_window(XMLConsoleWindow, account) - if window is None: - XMLConsoleWindow(account) - else: - window.present() + XMLConsoleWindow() +
def on_manage_proxies(action, param):
===================================== gajim/application.py ===================================== @@ -431,6 +431,7 @@ class GajimApplication(Gtk.Application): ('history-manager', app_actions.on_history_manager), ('preferences', app_actions.on_preferences), ('plugins', app_actions.on_plugins), + ('xml-console', app_actions.on_xml_console), ('file-transfer', app_actions.on_file_transfers), ('history', app_actions.on_history), ('shortcuts', app_actions.on_keyboard_shortcuts), @@ -496,9 +497,7 @@ class GajimApplication(Gtk.Application): from gajim import app_actions as a
if account == 'Local': - return [ - ('-xml-console', a.on_xml_console, 'always', 's') - ] + return []
return [ ('-bookmarks', a.on_bookmarks, 'online', 's'), @@ -507,7 +506,6 @@ class GajimApplication(Gtk.Application): ('-add-contact', a.on_add_contact, 'online', 'as'), ('-services', a.on_service_disco, 'online', 's'), ('-profile', a.on_profile, 'feature', 's'), - ('-xml-console', a.on_xml_console, 'always', 's'), ('-server-info', a.on_server_info, 'online', 's'), ('-archive', a.on_mam_preferences, 'feature', 's'), ('-pep-config', a.on_pep_config, 'online', 's'),
===================================== gajim/chat_control.py ===================================== @@ -1284,7 +1284,8 @@ class ChatControl(ChatControlBase): self.add_message(event.message, kind, tim=event.time, encrypted=event.encrypted, subject=event.subject, xhtml=event.xhtml, displaymarking=event.displaymarking, - correct_id=event.correct_id, additional_data=event.additional_data) + correct_id=event.correct_id, message_id=event.message_id, + additional_data=event.additional_data) if isinstance(event.msg_log_id, int): message_ids.append(event.msg_log_id)
===================================== gajim/chat_control_base.py ===================================== @@ -322,6 +322,9 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): self.received_history_pos = 0 self.orig_msg = None
+ # For XEP-0333 + self.last_msg_id = None + self.set_emoticon_popover()
# Attach speller @@ -1022,6 +1025,9 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): if restored: return
+ if message_id: + self.last_msg_id = message_id + if kind == 'incoming': if not self.type_id == message_control.TYPE_GC or \ app.config.notify_for_muc(jid) or \ @@ -1065,6 +1071,7 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): event_type.type_, self.contact.jid)
event = event_type(text, subject, self, msg_log_id, + msg_id=message_id, show_in_roster=show_in_roster, show_in_systray=show_in_systray) app.events.add_event(self.account, full_jid, event) @@ -1220,6 +1227,11 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): types=type_): # There were events to remove self.redraw_after_event_removed(jid) + # XEP-0333 Send <displayed> tag. + con.get_module('ChatMarkers').send_displayed_marker( + self.get_full_jid(), self.last_msg_id, + self.type_id == message_control.TYPE_GC) + self.last_msg_id = None # send chatstate inactive to the one we're leaving # and active to the one we visit if self.msg_textview.has_text(): @@ -1262,8 +1274,14 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): # we are at the end if not app.events.remove_events( self.account, jid, types=types_list): + con = app.connections[self.account] # There were events to remove self.redraw_after_event_removed(jid) + # XEP-0333 Send <displayed> tag. + con.get_module('ChatMarkers').send_displayed_marker( + self.get_full_jid(), self.last_msg_id, + self.type_id == message_control.TYPE_GC) + self.last_msg_id = None
def _on_scrollbar_button_release(self, scrollbar, event): if event.get_button()[1] != 1: @@ -1328,6 +1346,8 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): We just removed a 'printed_*' event, redraw contact in roster or gc_roster and titles in roster and msg_win """ + if not self.parent_win: # minimized groupchat + return self.parent_win.redraw_tab(self) self.parent_win.show_title() # TODO : get the contact and check get_show_in_roster()
===================================== gajim/common/app.py ===================================== @@ -152,6 +152,7 @@ gajim_common_features = [ nbxmpp.NS_BYTESTREAM, nbxmpp.NS_FILE, nbxmpp.NS_MUC, + nbxmpp.NS_CHATMARKERS, nbxmpp.NS_COMMANDS, nbxmpp.NS_DISCO_INFO, nbxmpp.NS_LAST,
===================================== gajim/common/connection.py ===================================== @@ -308,6 +308,9 @@ class CommonConnection: obj.session.last_send = time.time() msg_iq.setThread(obj.session.thread_id)
+ if obj.message: + msg_iq.setMarkable() + self._push_stanza_message_outgoing(obj, msg_iq)
def _push_stanza_message_outgoing(self, obj, msg_iq):
===================================== gajim/common/events.py ===================================== @@ -62,7 +62,7 @@ class Event: class ChatEvent(Event): type_ = 'chat' def __init__(self, message, subject, kind, time_, encrypted, resource, - msg_log_id, correct_id=None, xhtml=None, session=None, form_node=None, + msg_log_id, correct_id=None, message_id=None, xhtml=None, session=None, form_node=None, displaymarking=None, sent_forwarded=False, show_in_roster=False, show_in_systray=True, additional_data=None): Event.__init__(self, time_, show_in_roster=show_in_roster, @@ -74,6 +74,7 @@ class ChatEvent(Event): self.encrypted = encrypted self.resource = resource self.msg_log_id = msg_log_id + self.message_id = message_id self.correct_id = correct_id self.xhtml = xhtml self.session = session @@ -94,13 +95,14 @@ class PmEvent(ChatEvent): class PrintedChatEvent(Event): type_ = 'printed_chat' def __init__(self, message, subject, control, msg_log_id, time_=None, - show_in_roster=False, show_in_systray=True): + message_id=None, show_in_roster=False, show_in_systray=True): Event.__init__(self, time_, show_in_roster=show_in_roster, show_in_systray=show_in_systray) self.message = message self.subject = subject self.control = control self.msg_log_id = msg_log_id + self.message_id = message_id
class PrintedGcMsgEvent(PrintedChatEvent): type_ = 'printed_gc_msg'
===================================== gajim/common/modules/__init__.py ===================================== @@ -40,6 +40,7 @@ MODULES = [ 'bookmarks', 'caps', 'carbons', + 'chat_markers', 'chatstates', 'delimiter', 'discovery',
===================================== gajim/common/modules/chat_markers.py ===================================== @@ -0,0 +1,70 @@ +# This file is part of Gajim. +# +# Gajim is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published +# by the Free Software Foundation; version 3 only. +# +# Gajim is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Gajim. If not, see http://www.gnu.org/licenses/. + +# Chat Markers (XEP-0333) + +import nbxmpp +from nbxmpp.structs import StanzaHandler + +from gajim.common import app +from gajim.common.nec import NetworkEvent +from gajim.common.modules.base import BaseModule + + +class ChatMarkers(BaseModule): + + _nbxmpp_extends = 'ChatMarkers' + + def __init__(self, con): + BaseModule.__init__(self, con) + + self.handlers = [ + StanzaHandler(name='message', + callback=self._process_message_marker, + ns=nbxmpp.NS_CHATMARKERS, + priority=48), + ] + + def _process_message_marker(self, _con, stanza, properties): + if not properties.is_displayed_marker: + return + + if properties.type.is_error: + return + + self._log.info('Displayed: %s, id: %s', + properties.jid, properties.marker.id) + + app.nec.push_outgoing_event( + NetworkEvent('displayed-marker-received', + account=self._account, + jid=properties.jid, + type=properties.type, + marker_id=properties.marker.id)) + + def send_marker(self, jid, marker, id_, is_gc): + if is_gc: + message = nbxmpp.Message(to=jid, typ='groupchat') + else: + message = nbxmpp.Message(to=jid, typ='chat') + message.setTag(marker, namespace=nbxmpp.NS_CHATMARKERS, + attrs={'id': id_}) + self._nbxmpp().send(message) + + def send_displayed_marker(self, jid, id_, is_gc): + self.send_marker(jid, 'displayed', id_, is_gc) + + +def get_instance(*args, **kwargs): + return ChatMarkers(*args, **kwargs), 'ChatMarkers'
===================================== gajim/common/modules/message.py ===================================== @@ -173,7 +173,7 @@ class Message(BaseModule): 'timestamp': properties.timestamp, 'delayed': properties.user_timestamp is not None, 'muc_pm': properties.is_muc_pm, - 'gc_control': gc_control + 'gc_control': gc_control, }
app.nec.push_incoming_event(NetworkEvent('update-client-info',
===================================== gajim/data/gui/application_menu.ui ===================================== @@ -28,6 +28,11 @@ </item> </section> <section> + <item> + <attribute name="label" translatable="yes">XML Console</attribute> + <attribute name="action">app.xml-console</attribute> + <attribute name="accel"><Primary><Shift>X</attribute> + </item> <item> <attribute name="label" translatable="yes">File Transfer</attribute> <attribute name="action">app.file-transfer</attribute>
===================================== gajim/data/gui/xml_console_window.ui → gajim/data/gui/xml_console.ui ===================================== @@ -75,16 +75,12 @@ <property name="visible">True</property> <property name="can_focus">False</property> <property name="title" translatable="yes">XML Console</property> - <property name="has_subtitle">False</property> <property name="show_close_button">True</property> <child> - <object class="GtkSwitch"> + <object class="GtkImage"> <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="halign">start</property> - <property name="valign">center</property> - <property name="active">True</property> - <signal name="notify::active" handler="_on_enable" swapped="no"/> + <property name="can_focus">False</property> + <property name="icon_name">utilities-terminal-symbolic</property> </object> </child> </object>
===================================== gajim/groupchat_control.py ===================================== @@ -847,7 +847,8 @@ class GroupchatControl(ChatControlBase):
def on_private_message(self, nick, sent, msg, tim, xhtml, session, msg_log_id=None, encrypted=False, - displaymarking=None): + displaymarking=None, message_id=None): + # Do we have a queue? fjid = self.room_jid + '/' + nick
@@ -862,7 +863,9 @@ class GroupchatControl(ChatControlBase): session=session, form_node=None, displaymarking=displaymarking, - sent_forwarded=sent) + sent_forwarded=sent, + message_id=message_id) + app.events.add_event(self.account, fjid, event)
autopopup = app.config.get('autopopup') @@ -1106,7 +1109,8 @@ class GroupchatControl(ChatControlBase): self.on_private_message( nick, obj.sent, obj.msgtxt, obj.timestamp, obj.xhtml, self.session, msg_log_id=obj.msg_log_id, - encrypted=obj.encrypted, displaymarking=obj.displaymarking) + encrypted=obj.encrypted, displaymarking=obj.displaymarking, + message_id=obj.message_id)
def _nec_ping(self, obj): if self.contact.jid != obj.contact.room_jid:
===================================== gajim/gtk/xml_console.py ===================================== @@ -35,7 +35,7 @@ from gajim.gtk.const import SettingType
class XMLConsoleWindow(Gtk.ApplicationWindow): - def __init__(self, account): + def __init__(self): Gtk.ApplicationWindow.__init__(self) self.set_application(app.app) self.set_position(Gtk.WindowPosition.CENTER) @@ -44,8 +44,7 @@ class XMLConsoleWindow(Gtk.ApplicationWindow): self.set_show_menubar(False) self.set_name('XMLConsoleWindow')
- self.account = account - self.enabled = True + self.selected_account = None self.presence = True self.message = True self.iq = True @@ -55,24 +54,21 @@ class XMLConsoleWindow(Gtk.ApplicationWindow): self.filter_dialog = None self.last_stanza = None
- self._ui = get_builder('xml_console_window.ui') - - jid = app.get_jid_from_account(account) + self._ui = get_builder('xml_console.ui') self.set_titlebar(self._ui.headerbar) - self._ui.headerbar.set_subtitle(jid) - + self._set_titlebar() self.add(self._ui.box)
self._ui.paned.set_position(self._ui.paned.get_property('max-position'))
button = get_image_button( - 'edit-clear-all-symbolic', _('Clear')) - button.connect('clicked', self._on_clear) + 'applications-system-symbolic', _('Filter')) + button.connect('clicked', self._on_filter_options) self._ui.actionbar.pack_start(button)
button = get_image_button( - 'applications-system-symbolic', _('Filter')) - button.connect('clicked', self._on_filter_options) + 'edit-clear-symbolic', _('Clear')) + button.connect('clicked', self._on_clear) self._ui.actionbar.pack_start(button)
button = get_image_button( @@ -84,7 +80,7 @@ class XMLConsoleWindow(Gtk.ApplicationWindow): button.connect('clicked', self._on_paste_last) self._ui.actionbar.pack_start(button)
- button = get_image_button('mail-send-symbolic', _('Send')) + button = Gtk.Button.new_with_mnemonic(_('_Send')) button.connect('clicked', self._on_send) self._ui.actionbar.pack_end(button)
@@ -105,6 +101,13 @@ class XMLConsoleWindow(Gtk.ApplicationWindow): app.ged.register_event_handler( 'stanza-sent', ged.GUI1, self._nec_stanza_sent)
+ def _set_titlebar(self): + if self.selected_account is None: + title = _('All Accounts') + else: + title = app.get_jid_from_account(self.selected_account) + self._ui.headerbar.set_subtitle(title) + def _create_tags(self): buffer_ = self._ui.textview.get_buffer() in_color = app.css_config.get_value( @@ -160,12 +163,12 @@ class XMLConsoleWindow(Gtk.ApplicationWindow): self._ui.input_entry.grab_focus()
def _on_send(self, *args): - if not app.account_is_connected(self.account): + if not app.account_is_connected(self.selected_account): # If offline or connecting ErrorDialog( _('Connection not available'), _('Please make sure you are connected with '%s'.') % - self.account) + self.selected_account) return buffer_ = self._ui.input_entry.get_buffer() begin_iter, end_iter = buffer_.get_bounds() @@ -184,7 +187,7 @@ class XMLConsoleWindow(Gtk.ApplicationWindow): # stream management node = nbxmpp.Protocol(node=stanza, attrs={'xmlns': 'jabber:client'}) - app.connections[self.account].connection.send(node) + app.connections[self.selected_account].connection.send(node) self.last_stanza = stanza buffer_.set_text('')
@@ -207,32 +210,46 @@ class XMLConsoleWindow(Gtk.ApplicationWindow): if self.filter_dialog: self.filter_dialog.present() return - options = [ + + # Get all accounts for settings combobox + accounts = app.get_accounts_sorted() + combo_accounts = [] + for account in accounts: + label = app.get_account_label(account) + combo_accounts.append((account, label)) + combo_accounts.insert(0, (None, _('All Accounts'))) + + settings = [ + Setting(SettingKind.COMBO, _('Account'), + SettingType.VALUE, self.selected_account, + callback=self._set_account, + props={'combo_items': combo_accounts}), + Setting(SettingKind.SWITCH, 'Presence', SettingType.VALUE, self.presence, - callback=self._on_option, data='presence'), + callback=self._on_setting, data='presence'),
Setting(SettingKind.SWITCH, 'Message', SettingType.VALUE, self.message, - callback=self._on_option, data='message'), + callback=self._on_setting, data='message'),
Setting(SettingKind.SWITCH, 'IQ', SettingType.VALUE, self.iq, - callback=self._on_option, data='iq'), + callback=self._on_setting, data='iq'),
Setting(SettingKind.SWITCH, 'Stream Management', SettingType.VALUE, self.stream, - callback=self._on_option, data='stream'), + callback=self._on_setting, data='stream'),
Setting(SettingKind.SWITCH, 'In', SettingType.VALUE, self.incoming, - callback=self._on_option, data='incoming'), + callback=self._on_setting, data='incoming'),
Setting(SettingKind.SWITCH, 'Out', SettingType.VALUE, self.outgoing, - callback=self._on_option, data='outgoing'), + callback=self._on_setting, data='outgoing'), ]
- self.filter_dialog = SettingsDialog(self, 'Filter', + self.filter_dialog = SettingsDialog(self, _('Filter'), Gtk.DialogFlags.DESTROY_WITH_PARENT, - options, self.account) + settings, self.selected_account) self.filter_dialog.connect('destroy', self._on_filter_destroyed)
def _on_filter_destroyed(self, win): @@ -247,10 +264,11 @@ class XMLConsoleWindow(Gtk.ApplicationWindow): app.ged.remove_event_handler( 'stanza-sent', ged.GUI1, self._nec_stanza_sent)
- def _on_enable(self, switch, param): - self.enabled = switch.get_active() + def _set_account(self, value, data): + self.selected_account = value + self._set_titlebar()
- def _on_option(self, value, data): + def _on_setting(self, value, data): setattr(self, data, value) value = not value table = self._ui.textview.get_buffer().get_tag_table() @@ -263,19 +281,21 @@ class XMLConsoleWindow(Gtk.ApplicationWindow): tag.set_property('invisible', value)
def _nec_stanza_received(self, obj): - if obj.conn.name != self.account: - return - self._print_stanza(obj.stanza_str, 'incoming') + if self.selected_account is not None: + if obj.conn.name != self.selected_account: + return + self._print_stanza(obj, 'incoming')
def _nec_stanza_sent(self, obj): - if obj.conn.name != self.account: - return - self._print_stanza(obj.stanza_str, 'outgoing') + if self.selected_account is not None: + if obj.conn.name != self.selected_account: + return + self._print_stanza(obj, 'outgoing')
- def _print_stanza(self, stanza, kind): + def _print_stanza(self, obj, kind): + account = app.get_jid_from_account(obj.conn.name) + stanza = obj.stanza_str # Kind must be 'incoming' or 'outgoing' - if not self.enabled: - return if not stanza: return
@@ -294,9 +314,10 @@ class XMLConsoleWindow(Gtk.ApplicationWindow): elif stanza.startswith('<r') or stanza.startswith('<a'): type_ = 'stream'
- stanza = '<!-- {kind} {time} -->\n{stanza}\n\n'.format( + stanza = '<!-- {kind} {time} ({account}) -->\n{stanza}\n\n'.format( kind=kind.capitalize(), time=time.strftime('%c'), + account=account, stanza=stanza.replace('><', '>\n<')) buffer_.insert_with_tags_by_name(end_iter, stanza, type_, kind)
===================================== gajim/gui_interface.py ===================================== @@ -285,6 +285,44 @@ class Interface: if ctrl and ctrl.session and len(obj.contact_list) > 1: ctrl.remove_session(ctrl.session)
+ def handle_event_chat_marker_received(self, event): + if event.type.is_groupchat: + types = ['printed_gc_msg', 'printed_marked_gc_msg'] + control = app.interface.msg_win_mgr.get_control(event.jid, + event.account) + if not control: + if event.jid in app.interface.minimized_controls[event.account]: + control = app.interface.minimized_controls[event.account][event.jid] + if not control: + return + if obj.resource != control.nick: + return + event_jid = event.jid + + else: + + types = ['chat', 'pm', 'printed_chat', 'printed_pm'] + if event.properties.carbon_type != 'sent': + return + + control = app.interface.msg_win_mgr.get_control(event.jid, + event.account) + if not control: + control = app.interface.msg_win_mgr.get_gc_control( + event.jid, event.account) + event_jid = event.jid + + # Compare with control.last_msg_id. + events_ = app.events.get_events(event.account, event_jid, types) + if not events_: + return + if events_[-1].msg_id != event.marker_id: + return + if not app.events.remove_events(event.account, event_jid, types=types): + # There were events to remove + if control: + control.redraw_after_event_removed(event.jid) + def handle_event_msgerror(self, obj): #'MSGERROR' (account, (jid, error_code, error_msg, msg, time[session])) account = obj.conn.name @@ -1173,6 +1211,7 @@ class Interface: 'unsubscribed-presence-received': [ self.handle_event_unsubscribed_presence], 'zeroconf-name-conflict': [self.handle_event_zc_name_conflict], + 'displayed-marker-received': [self.handle_event_chat_marker_received], }
def register_core_handlers(self):
===================================== gajim/gui_menu_builder.py ===================================== @@ -678,8 +678,6 @@ def get_account_menu(account): ('-pep-config', _('PEP Configuration')), ('-sync-history', _('Synchronise History')), ('-privacylists', _('Privacy Lists')), - - ('-xml-console', _('XML Console')) ]), (_('Admin'), [ ('-send-server-message', _('Send Server Message…')), @@ -689,10 +687,6 @@ def get_account_menu(account): ]), ]
- zeroconf_menu = [ - ('-xml-console', _('XML Console')), - ] - def build_menu(preset): menu = Gio.Menu() for item in preset: @@ -713,8 +707,6 @@ def get_account_menu(account): menu.append_submenu(label, submenu) return menu
- if account == 'Local': - return build_menu(zeroconf_menu) return build_menu(account_menu)
@@ -741,8 +733,9 @@ def build_accounts_menu(): acc_menu.append_item(modify_account_item) for acc in accounts_list: label = escape_mnemonic(app.get_account_label(acc)) - acc_menu.append_submenu( - label, get_account_menu(acc)) + if acc != 'Local': + acc_menu.append_submenu( + label, get_account_menu(acc)) else: acc_menu = get_account_menu(accounts_list[0]) modify_account_item = Gio.MenuItem.new(_('_Modify Account…'),
===================================== gajim/session.py ===================================== @@ -253,7 +253,9 @@ class ChatControlSession: if do_event: event = event_t(obj.msgtxt, obj.subject, obj.mtype, obj.timestamp, obj.encrypted, obj.resource, obj.msg_log_id, - correct_id=(obj.id_, obj.correct_id), xhtml=obj.xhtml, + correct_id=obj.correct_id, + message_id=obj.message_id, + xhtml=obj.xhtml, session=self, form_node=obj.form_node, displaymarking=obj.displaymarking, sent_forwarded=obj.forwarded and obj.sent,
View it on GitLab: https://dev.gajim.org/gajim/gajim/compare/d6f64b8ee9ed598a6eb3b552a816ad5685...