Refactor groupchat nick auto completion
This commit is contained in:
parent
a00e8e3abb
commit
9d8b56bc0f
|
@ -8,6 +8,7 @@
|
||||||
# Stephan Erb <steve-e AT h3c.de>
|
# Stephan Erb <steve-e AT h3c.de>
|
||||||
# Copyright (C) 2008 Brendan Taylor <whateley AT gmail.com>
|
# Copyright (C) 2008 Brendan Taylor <whateley AT gmail.com>
|
||||||
# Jonathan Schleifer <js-gajim AT webkeks.org>
|
# Jonathan Schleifer <js-gajim AT webkeks.org>
|
||||||
|
# Copyright (C) 2018 Marcin Mielniczuk <marmistrz dot dev at zoho dot eu>
|
||||||
#
|
#
|
||||||
# This file is part of Gajim.
|
# This file is part of Gajim.
|
||||||
#
|
#
|
||||||
|
@ -32,6 +33,7 @@ import logging
|
||||||
from enum import IntEnum, unique
|
from enum import IntEnum, unique
|
||||||
|
|
||||||
import nbxmpp
|
import nbxmpp
|
||||||
|
|
||||||
from gi.repository import Gtk
|
from gi.repository import Gtk
|
||||||
from gi.repository import Gdk
|
from gi.repository import Gdk
|
||||||
from gi.repository import Pango
|
from gi.repository import Pango
|
||||||
|
@ -75,6 +77,7 @@ from gajim.gtk.add_contact import AddNewContactWindow
|
||||||
from gajim.gtk.tooltips import GCTooltip
|
from gajim.gtk.tooltips import GCTooltip
|
||||||
from gajim.gtk.groupchat_config import GroupchatConfig
|
from gajim.gtk.groupchat_config import GroupchatConfig
|
||||||
from gajim.gtk.adhoc_commands import CommandWindow
|
from gajim.gtk.adhoc_commands import CommandWindow
|
||||||
|
from gajim.gtk.util import NickCompletionGenerator
|
||||||
from gajim.gtk.util import get_icon_name
|
from gajim.gtk.util import get_icon_name
|
||||||
from gajim.gtk.util import get_affiliation_surface
|
from gajim.gtk.util import get_affiliation_surface
|
||||||
from gajim.gtk.util import get_builder
|
from gajim.gtk.util import get_builder
|
||||||
|
@ -317,6 +320,7 @@ class GroupchatControl(ChatControlBase):
|
||||||
# sorted list of nicks who mentioned us (last at the end)
|
# sorted list of nicks who mentioned us (last at the end)
|
||||||
self.attention_list = []
|
self.attention_list = []
|
||||||
self.nick_hits = []
|
self.nick_hits = []
|
||||||
|
self._nick_completion = NickCompletionGenerator(self.nick)
|
||||||
self.last_key_tabs = False
|
self.last_key_tabs = False
|
||||||
|
|
||||||
self.subject = ''
|
self.subject = ''
|
||||||
|
@ -1433,11 +1437,7 @@ class GroupchatControl(ChatControlBase):
|
||||||
other_tags_for_name.append('bold')
|
other_tags_for_name.append('bold')
|
||||||
other_tags_for_text.append('marked')
|
other_tags_for_text.append('marked')
|
||||||
|
|
||||||
if contact in self.attention_list:
|
self._nick_completion.record_message(contact, highlight)
|
||||||
self.attention_list.remove(contact)
|
|
||||||
elif len(self.attention_list) > 6:
|
|
||||||
self.attention_list.pop(0) # remove older
|
|
||||||
self.attention_list.append(contact)
|
|
||||||
|
|
||||||
if text.startswith('/me ') or text.startswith('/me\n'):
|
if text.startswith('/me ') or text.startswith('/me\n'):
|
||||||
other_tags_for_text.append('gc_nickname_color_' + \
|
other_tags_for_text.append('gc_nickname_color_' + \
|
||||||
|
@ -1826,6 +1826,10 @@ class GroupchatControl(ChatControlBase):
|
||||||
for role in ('visitor', 'participant', 'moderator'):
|
for role in ('visitor', 'participant', 'moderator'):
|
||||||
self.draw_role(role)
|
self.draw_role(role)
|
||||||
|
|
||||||
|
def _change_nick(self, new_nick: str) -> None:
|
||||||
|
self.nick = new_nick
|
||||||
|
self._nick_completion.change_nick(new_nick)
|
||||||
|
|
||||||
def _nec_gc_presence_received(self, obj):
|
def _nec_gc_presence_received(self, obj):
|
||||||
if obj.room_jid != self.room_jid or obj.conn.name != self.account:
|
if obj.room_jid != self.room_jid or obj.conn.name != self.account:
|
||||||
return
|
return
|
||||||
|
@ -1945,7 +1949,7 @@ class GroupchatControl(ChatControlBase):
|
||||||
elif '303' in obj.status_code: # Someone changed their nick
|
elif '303' in obj.status_code: # Someone changed their nick
|
||||||
if obj.new_nick == self.new_nick or obj.nick == self.nick:
|
if obj.new_nick == self.new_nick or obj.nick == self.nick:
|
||||||
# We changed our nick
|
# We changed our nick
|
||||||
self.nick = obj.new_nick
|
self.change_nick(obj.new_nick)
|
||||||
self.new_nick = ''
|
self.new_nick = ''
|
||||||
s = _('You are now known as %s') % self.nick
|
s = _('You are now known as %s') % self.nick
|
||||||
else:
|
else:
|
||||||
|
@ -1963,8 +1967,7 @@ class GroupchatControl(ChatControlBase):
|
||||||
# after that, but that doesn't hurt
|
# after that, but that doesn't hurt
|
||||||
self.add_contact_to_roster(obj.new_nick, obj.show, role,
|
self.add_contact_to_roster(obj.new_nick, obj.show, role,
|
||||||
affiliation, obj.status, obj.real_jid)
|
affiliation, obj.status, obj.real_jid)
|
||||||
if obj.nick in self.attention_list:
|
self._nick_completion.contact_renamed(nick, obj.new_nick)
|
||||||
self.attention_list.remove(obj.nick)
|
|
||||||
# keep nickname color
|
# keep nickname color
|
||||||
if obj.nick in self.gc_custom_colors:
|
if obj.nick in self.gc_custom_colors:
|
||||||
self.gc_custom_colors[obj.new_nick] = \
|
self.gc_custom_colors[obj.new_nick] = \
|
||||||
|
@ -2016,7 +2019,7 @@ class GroupchatControl(ChatControlBase):
|
||||||
if not iter_:
|
if not iter_:
|
||||||
if '210' in obj.status_code:
|
if '210' in obj.status_code:
|
||||||
# Server changed our nick
|
# Server changed our nick
|
||||||
self.nick = obj.nick
|
self.change_nick(obj.nick)
|
||||||
s = _('You are now known as %s') % nick
|
s = _('You are now known as %s') % nick
|
||||||
self.print_conversation(s, 'info', graphics=False)
|
self.print_conversation(s, 'info', graphics=False)
|
||||||
iter_ = self.add_contact_to_roster(obj.nick, obj.show, role,
|
iter_ = self.add_contact_to_roster(obj.nick, obj.show, role,
|
||||||
|
@ -2080,9 +2083,6 @@ class GroupchatControl(ChatControlBase):
|
||||||
right_changed:
|
right_changed:
|
||||||
st = ''
|
st = ''
|
||||||
|
|
||||||
if obj.show == 'offline':
|
|
||||||
if obj.nick in self.attention_list:
|
|
||||||
self.attention_list.remove(obj.nick)
|
|
||||||
if obj.show == 'offline' and print_status in ('all', 'in_and_out') \
|
if obj.show == 'offline' and print_status in ('all', 'in_and_out') \
|
||||||
and (not obj.status_code or '307' not in obj.status_code):
|
and (not obj.status_code or '307' not in obj.status_code):
|
||||||
st = _('%s has left') % nick_jid
|
st = _('%s has left') % nick_jid
|
||||||
|
@ -2469,6 +2469,10 @@ class GroupchatControl(ChatControlBase):
|
||||||
self.print_conversation(_('%(jid)s has been invited in this room') %
|
self.print_conversation(_('%(jid)s has been invited in this room') %
|
||||||
{'jid': contact_jid}, graphics=False)
|
{'jid': contact_jid}, graphics=False)
|
||||||
|
|
||||||
|
def _jid_not_blocked(self, bare_jid: str) -> bool:
|
||||||
|
fjid = self.room_jid + '/' + bare_jid
|
||||||
|
return not helpers.jid_is_blocked(self.account, fjid)
|
||||||
|
|
||||||
def _on_message_textview_key_press_event(self, widget, event):
|
def _on_message_textview_key_press_event(self, widget, event):
|
||||||
res = ChatControlBase._on_message_textview_key_press_event(self, widget,
|
res = ChatControlBase._on_message_textview_key_press_event(self, widget,
|
||||||
event)
|
event)
|
||||||
|
@ -2508,25 +2512,17 @@ class GroupchatControl(ChatControlBase):
|
||||||
self.nick_hits.append(self.nick_hits[0])
|
self.nick_hits.append(self.nick_hits[0])
|
||||||
begin = self.nick_hits.pop(0)
|
begin = self.nick_hits.pop(0)
|
||||||
else:
|
else:
|
||||||
self.nick_hits = [] # clear the hit list
|
|
||||||
list_nick = app.contacts.get_nick_list(self.account,
|
list_nick = app.contacts.get_nick_list(self.account,
|
||||||
self.room_jid)
|
self.room_jid)
|
||||||
list_nick.sort(key=str.lower) # case-insensitive sort
|
list_nick = list(filter(self._jid_not_blocked, list_nick))
|
||||||
if begin == '':
|
|
||||||
# empty message, show lasts nicks that highlighted us first
|
log.debug("Nicks to be considered for autosuggestions: %s",
|
||||||
for nick in self.attention_list:
|
list_nick)
|
||||||
if nick in list_nick:
|
self.nick_hits = self._nick_completion.generate_suggestions(
|
||||||
list_nick.remove(nick)
|
nicks=list_nick, beginning=begin)
|
||||||
list_nick.insert(0, nick)
|
log.debug("Nicks filtered for autosuggestions: %s",
|
||||||
|
self.nick_hits)
|
||||||
|
|
||||||
if self.nick in list_nick:
|
|
||||||
list_nick.remove(self.nick) # Skip self
|
|
||||||
for nick in list_nick:
|
|
||||||
fjid = self.room_jid + '/' + nick
|
|
||||||
if nick.lower().startswith(begin.lower()) and not \
|
|
||||||
helpers.jid_is_blocked(self.account, fjid):
|
|
||||||
# the word is the beginning of a nick
|
|
||||||
self.nick_hits.append(nick)
|
|
||||||
if self.nick_hits:
|
if self.nick_hits:
|
||||||
if len(splitted_text) < 2 or with_refer_to_nick_char:
|
if len(splitted_text) < 2 or with_refer_to_nick_char:
|
||||||
# This is the 1st word of the line or no word or we are cycling
|
# This is the 1st word of the line or no word or we are cycling
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
# Copyright (C) 2018 Marcin Mielniczuk <marmistrz.dev AT zoho.eu>
|
||||||
|
# Copyright (C) 2018 Philipp Hörist <philipp AT hoerist.com>
|
||||||
|
#
|
||||||
# This file is part of Gajim.
|
# This file is part of Gajim.
|
||||||
#
|
#
|
||||||
# Gajim is free software; you can redistribute it and/or modify
|
# Gajim is free software; you can redistribute it and/or modify
|
||||||
|
@ -42,6 +45,89 @@ if _icon_theme is not None:
|
||||||
log = logging.getLogger('gajim.gtk.util')
|
log = logging.getLogger('gajim.gtk.util')
|
||||||
|
|
||||||
|
|
||||||
|
class NickCompletionGenerator:
|
||||||
|
def __init__(self, self_nick: str) -> None:
|
||||||
|
self.nick = self_nick
|
||||||
|
self.sender_list = [] # type: List[str]
|
||||||
|
self.attention_list = [] # type: List[str]
|
||||||
|
|
||||||
|
def change_nick(self, new_nick: str) -> None:
|
||||||
|
self.nick = new_nick
|
||||||
|
|
||||||
|
def record_message(self, contact: str, highlight: bool) -> None:
|
||||||
|
if contact == self.nick:
|
||||||
|
return
|
||||||
|
|
||||||
|
log.debug('Recorded a message from %s, highlight; %s', contact,
|
||||||
|
highlight)
|
||||||
|
if highlight:
|
||||||
|
try:
|
||||||
|
self.attention_list.remove(contact)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
if len(self.attention_list) > 6:
|
||||||
|
self.attention_list.pop(0) # remove older
|
||||||
|
self.attention_list.append(contact)
|
||||||
|
|
||||||
|
# TODO implement it in a more efficient way
|
||||||
|
# Currently it's O(n*m + n*s), where n is the number of participants and
|
||||||
|
# m is the number of messages processed, s - the number of times the
|
||||||
|
# suggestions are requested
|
||||||
|
#
|
||||||
|
# A better way to do it would be to keep a dict: contact -> timestamp
|
||||||
|
# with expected O(1) insert, and sort it by timestamps in O(n log n)
|
||||||
|
# for each suggestion (currently generating the suggestions is O(n))
|
||||||
|
# this would give the expected complexity of O(m + s * n log n)
|
||||||
|
try:
|
||||||
|
self.sender_list.remove(contact)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
self.sender_list.append(contact)
|
||||||
|
|
||||||
|
def contact_renamed(self, contact_old: str, contact_new: str) -> None:
|
||||||
|
log.debug('Contact %s renamed to %s', contact_old, contact_new)
|
||||||
|
for lst in (self.attention_list, self.sender_list):
|
||||||
|
for idx, contact in enumerate(lst):
|
||||||
|
if contact == contact_old:
|
||||||
|
lst[idx] = contact_new
|
||||||
|
|
||||||
|
|
||||||
|
def generate_suggestions(self, nicks: List[str],
|
||||||
|
beginning: str) -> List[str]:
|
||||||
|
"""
|
||||||
|
Generate the order of suggested MUC autocompletions
|
||||||
|
|
||||||
|
`nicks` is the list of contacts currently participating in a MUC
|
||||||
|
`beginning` is the text already typed by the user
|
||||||
|
"""
|
||||||
|
def nick_matching(nick: str) -> bool:
|
||||||
|
return nick != self.nick \
|
||||||
|
and nick.lower().startswith(beginning.lower())
|
||||||
|
|
||||||
|
if beginning == '':
|
||||||
|
# empty message, so just suggest recent mentions
|
||||||
|
potential_matches = self.attention_list
|
||||||
|
else:
|
||||||
|
# nick partially typed, try completing it
|
||||||
|
potential_matches = self.sender_list
|
||||||
|
|
||||||
|
potential_matches_set = set(potential_matches)
|
||||||
|
log.debug('Priority matches: %s', potential_matches_set)
|
||||||
|
|
||||||
|
matches = [n for n in potential_matches if nick_matching(n)]
|
||||||
|
# the most recent nick is the last one on the list
|
||||||
|
matches.reverse()
|
||||||
|
|
||||||
|
# handle people who have not posted/mentioned us
|
||||||
|
other_nicks = [
|
||||||
|
n for n in nicks if nick_matching(n) and n not in potential_matches_set
|
||||||
|
]
|
||||||
|
other_nicks.sort(key=str.lower)
|
||||||
|
log.debug('Other matches: %s', other_nicks)
|
||||||
|
|
||||||
|
return matches + other_nicks
|
||||||
|
|
||||||
|
|
||||||
class Builder:
|
class Builder:
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
filename: str,
|
filename: str,
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from gajim.gtk.util import NickCompletionGenerator
|
||||||
|
|
||||||
|
class Test(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_generate_suggestions(self):
|
||||||
|
gen = NickCompletionGenerator('meeeee')
|
||||||
|
|
||||||
|
l = ['aaaa', 'meeeee', 'fooo', 'xxxxz', 'xaaaz']
|
||||||
|
for n in l:
|
||||||
|
gen.record_message(n, False)
|
||||||
|
l2 = ['xxx'] + l
|
||||||
|
r = gen.generate_suggestions(nicks=l2, beginning='x')
|
||||||
|
self.assertEqual(r, ['xaaaz', 'xxxxz', 'xxx'])
|
||||||
|
|
||||||
|
r = gen.generate_suggestions(
|
||||||
|
nicks=l2,
|
||||||
|
beginning='m'
|
||||||
|
)
|
||||||
|
self.assertEqual(r, [])
|
||||||
|
|
||||||
|
for n in ['xaaaz', 'xxxxz']:
|
||||||
|
gen.record_message(n, True)
|
||||||
|
|
||||||
|
r = gen.generate_suggestions(
|
||||||
|
nicks=l2,
|
||||||
|
beginning='x'
|
||||||
|
)
|
||||||
|
self.assertEqual(r, ['xxxxz', 'xaaaz', 'xxx'])
|
||||||
|
r = gen.generate_suggestions(
|
||||||
|
nicks=l2,
|
||||||
|
beginning=''
|
||||||
|
)
|
||||||
|
self.assertEqual(r, ['xxxxz', 'xaaaz', 'aaaa', 'fooo', 'xxx'])
|
||||||
|
|
||||||
|
l2[1] = 'bbbb'
|
||||||
|
gen.contact_renamed('aaaa', 'bbbb')
|
||||||
|
r = gen.generate_suggestions(
|
||||||
|
nicks=l2,
|
||||||
|
beginning=''
|
||||||
|
)
|
||||||
|
self.assertEqual(r, ['xxxxz', 'xaaaz', 'bbbb', 'fooo', 'xxx'])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
Loading…
Reference in New Issue