Welcome to mirror list, hosted at ThFree Co, Russian Federation.

dev.gajim.org/gajim/gajim.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorwurstsalat <mailtrash@posteo.de>2022-10-02 22:03:57 +0300
committerwurstsalat <mailtrash@posteo.de>2022-10-03 10:43:57 +0300
commit9b2922a3594d22da2de39c4c95a0a5f3a4215bdf (patch)
treeee024962f537c2fcbf5a8ca8475937b53055608d
parent1f2dbcad7483641c639015a5d96ade78c6f264f9 (diff)
imprv: GroupChatNickCompletion: Simplify suggestions
- remove shell_like_completion - query nicks from DB (no state needed) Fixes #11155
-rw-r--r--gajim/common/setting_values.py5
-rw-r--r--gajim/common/storage/archive.py33
-rw-r--r--gajim/gtk/groupchat_nick_completion.py345
3 files changed, 137 insertions, 246 deletions
diff --git a/gajim/common/setting_values.py b/gajim/common/setting_values.py
index 94973d27c..e2d9319f6 100644
--- a/gajim/common/setting_values.py
+++ b/gajim/common/setting_values.py
@@ -79,7 +79,6 @@ BoolSettings = Literal[
'remote_control',
'save_main_window_position',
'send_on_ctrl_enter',
- 'shell_like_completion',
'show_chatstate_in_banner',
'show_help_start_chat',
'show_in_taskbar',
@@ -253,7 +252,6 @@ APP_SETTINGS = {
'save_main_window_position': True,
'search_engine': 'https://duckduckgo.com/?q=%s',
'send_on_ctrl_enter': False,
- 'shell_like_completion': False,
'show_chatstate_in_banner': True,
'show_help_start_chat': True,
'show_in_taskbar': True,
@@ -696,9 +694,6 @@ ADVANCED_SETTINGS = {
'search_engine': '',
'send_on_ctrl_enter': _(
'Send message on Ctrl+Enter and make a new line with Enter.'),
- 'shell_like_completion': _(
- 'If enabled, completion in group chats will be like a shell '
- 'auto-completion.'),
'stun_server': _('STUN server to use when using Jingle'),
'trayicon_notification_on_events': _(
'Notify of events in the notification area.'),
diff --git a/gajim/common/storage/archive.py b/gajim/common/storage/archive.py
index bb2926482..bf0092f03 100644
--- a/gajim/common/storage/archive.py
+++ b/gajim/common/storage/archive.py
@@ -45,6 +45,7 @@ from gajim.common.helpers import AdditionalDataDict
from gajim.common.const import MAX_MESSAGE_CORRECTION_DELAY
from gajim.common.const import KindConstant
from gajim.common.const import JIDConstant
+from gajim.common.modules.contacts import GroupchatContact
from gajim.common.storage.base import SqliteStorage
from gajim.common.storage.base import timeit
@@ -832,6 +833,38 @@ class MessageArchiveStorage(SqliteStorage):
tuple(jids) + (date_ts, delta_ts)).fetchone()
@timeit
+ def get_recent_muc_nicks(self, contact: GroupchatContact) -> list[str]:
+ '''
+ Queries the last 50 message rows and gathers nicknames in a list
+ '''
+ jids = [contact.jid]
+ account_id = self.get_account_id(contact.account)
+ kinds = map(str, [KindConstant.STATUS,
+ KindConstant.GCSTATUS,
+ KindConstant.ERROR])
+
+ sql = '''
+ SELECT contact_name
+ FROM logs NATURAL JOIN jids WHERE jid IN ({jids})
+ AND account_id = {account_id}
+ AND kind NOT IN ({kinds})
+ ORDER BY time DESC
+ '''.format(jids=', '.join('?' * len(jids)),
+ account_id=account_id,
+ kinds=', '.join(kinds))
+
+ result = self._con.execute(
+ sql,
+ tuple(jids)).fetchmany(50)
+
+ nicknames: list[str] = []
+ for row in result:
+ if row.contact_name not in nicknames:
+ nicknames.append(row.contact_name)
+
+ return nicknames
+
+ @timeit
def deduplicate_muc_message(self,
account: str,
jid: str,
diff --git a/gajim/gtk/groupchat_nick_completion.py b/gajim/gtk/groupchat_nick_completion.py
index 89513aca1..ac52f6aae 100644
--- a/gajim/gtk/groupchat_nick_completion.py
+++ b/gajim/gtk/groupchat_nick_completion.py
@@ -18,19 +18,15 @@ from typing import Optional
import logging
-
from gi.repository import Gtk
from gi.repository import Gdk
from gajim.common import app
from gajim.common import ged
-from gajim.common import types
from gajim.common.events import GcMessageReceived
-from gajim.common.events import MUCNicknameChanged
from gajim.common.ged import EventHelper
from gajim.common.helpers import jid_is_blocked
-from gajim.common.helpers import message_needs_highlight
-
+from gajim.common.modules.contacts import GroupchatContact
log = logging.getLogger('gajim.gui.groupchat_nick_completion')
@@ -39,154 +35,24 @@ class GroupChatNickCompletion(EventHelper):
def __init__(self) -> None:
EventHelper.__init__(self)
- self._account: Optional[str] = None
- self._contact: Optional[types.GroupchatContactT] = None
+ self._contact: Optional[GroupchatContact] = None
- self._sender_list: list[str] = []
- self._highlight_list: list[str] = []
- self._nick_hits: list[str] = []
+ self._suggestions: list[str] = []
self._last_key_tab = False
- self._nick_data: dict[str, tuple[list[str], list[str]]] = {}
-
self.register_event(
- 'gc-message-received', ged.GUI1, self._on_gc_message_received)
+ 'gc-message-received', ged.GUI2, self._on_gc_message_received)
- def switch_contact(self, contact: types.GroupchatContactT) -> None:
- self._nick_hits.clear()
+ def switch_contact(self, contact: GroupchatContact) -> None:
+ self._suggestions.clear()
self._last_key_tab = False
-
- if self._contact is not None:
- self._contact.disconnect_all_from_obj(self)
- self._nick_data[str(self._contact.jid)] = (
- self._sender_list, self._highlight_list)
-
- nick_data = self._nick_data.get(str(contact.jid))
- if nick_data is None:
- self._sender_list.clear()
- self._highlight_list.clear()
- else:
- self._sender_list, self._highlight_list = nick_data
-
- self._account = contact.account
self._contact = contact
- self._contact.connect(
- 'user-nickname-changed', self._on_user_nickname_changed)
-
- def _on_user_nickname_changed(self,
- _contact: types.GroupchatContact,
- _signal_name: str,
- event: MUCNicknameChanged,
- old_contact: types.GroupchatParticipant,
- new_contact: types.GroupchatParticipant
- ) -> None:
-
- if event.is_self:
- return
-
- old_name = old_contact.name
- new_name = new_contact.name
-
- log.debug('Contact %s renamed to %s', old_name, new_name)
- for lst in (self._highlight_list, self._sender_list):
- for idx, contact in enumerate(lst):
- if contact == old_name:
- lst[idx] = new_name
-
- def _on_gc_message_received(self, event: GcMessageReceived) -> None:
- if event.properties.muc_nickname is None:
- # Message from server
- return
-
- client = app.get_client(event.account)
- gc_contact = client.get_module('Contacts').get_contact(
- event.room_jid)
-
- participant_nick = event.properties.muc_nickname
- if participant_nick == gc_contact.nickname:
- return
-
- highlight = message_needs_highlight(
- event.msgtxt, gc_contact.nickname, client.get_own_jid().bare)
- self._process_message(participant_nick, highlight, event.room_jid)
-
- def _process_message(self,
- participant_nick: str,
- highlight: bool,
- room_jid: str
- ) -> None:
- nick_data = self._nick_data.get(room_jid)
- if nick_data is None:
- return
-
- sender_list, highlight_list = nick_data
- if highlight:
- try:
- highlight_list.remove(participant_nick)
- except ValueError:
- pass
- if len(highlight_list) > 6:
- highlight_list.pop(0) # remove older
- highlight_list.append(participant_nick)
-
- # 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:
- sender_list.remove(participant_nick)
- except ValueError:
- pass
- sender_list.append(participant_nick)
-
- 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:
- assert self._contact
- return (nick != self._contact.nickname and
- nick.lower().startswith(beginning.lower()))
-
- if beginning == '':
- # empty message, so just suggest recent mentions
- potential_matches = self._highlight_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
def process_key_press(self,
textview: Gtk.TextView,
event: Gdk.EventKey
) -> bool:
+
if (event.get_state() & Gdk.ModifierType.SHIFT_MASK or
event.get_state() & Gdk.ModifierType.CONTROL_MASK or
event.keyval not in (Gdk.KEY_ISO_Left_Tab, Gdk.KEY_Tab)):
@@ -199,111 +65,108 @@ class GroupChatNickCompletion(EventHelper):
end_iter = message_buffer.get_iter_at_mark(cursor_position)
text = message_buffer.get_text(start_iter, end_iter, False)
- text_split = text.split()
+ if text.split():
+ # Store last word for autocompletion
+ prefix = text.split()[-1]
+ else:
+ prefix = ''
+
+ # Configurable string to be displayed after the nick:
+ # e.g. "user," or "user:"
+ ref_ext = app.settings.get('gc_refer_to_nick_char')
+ has_ref_ext = False
+
+ # Default suffix to 1: space printed after completion
+ suffix_len = 1
+
+ if ref_ext and text.endswith(ref_ext + ' '):
+ has_ref_ext = True
+ suffix_len = len(ref_ext + ' ')
+
+ if not self._last_key_tab or not self._suggestions:
+ self._suggestions = self._generate_suggestions(prefix)
- # check if tab is pressed with empty message
- if text_split: # if there are any words
- begin = text_split[-1] # last word we typed
+ if not self._suggestions:
+ self._last_key_tab = True
+ return False
+
+ if (self._last_key_tab and
+ text[:-suffix_len].endswith(self._suggestions[0])):
+ # Cycle suggestions list
+ self._suggestions.append(self._suggestions[0])
+ prefix = self._suggestions.pop(0)
+
+ if len(text.split()) < 2 or has_ref_ext:
+ suffix = ref_ext + ' '
else:
- begin = ''
-
- gc_refer_to_nick_char = app.settings.get('gc_refer_to_nick_char')
- with_refer_to_nick_char = False
- after_nick_len = 1 # the space that is printed after we type [Tab]
-
- # first part of this if : works fine even if refer_to_nick_char
- if (gc_refer_to_nick_char and
- text.endswith(gc_refer_to_nick_char + ' ')):
- with_refer_to_nick_char = True
- after_nick_len = len(gc_refer_to_nick_char + ' ')
- if (self._nick_hits and self._last_key_tab and
- text[:-after_nick_len].endswith(self._nick_hits[0])):
- # we should cycle
- # Previous nick in list may had a space inside, so we check text
- # and not text_split and store it into 'begin' var
- self._nick_hits.append(self._nick_hits[0])
- begin = self._nick_hits.pop(0)
+ suffix = ' '
+
+ start_iter = end_iter.copy()
+ if (self._last_key_tab and has_ref_ext or (text and text[-1] == ' ')):
+ # Mind the added space from last completion;
+ # ref_ext may also consist of more than one char
+ start_iter.backward_chars(len(prefix) + len(suffix))
else:
- assert self._contact
- list_nick = self._contact.get_user_nicknames()
- list_nick = list(filter(self._jid_not_blocked, list_nick))
-
- log.debug('Nicks to be considered for autosuggestions: %s',
- list_nick)
- self._nick_hits = self._generate_suggestions(
- nicks=list_nick, beginning=begin)
- log.debug('Nicks filtered for autosuggestions: %s',
- self._nick_hits)
- if not self._nick_hits:
- self._last_key_tab = True
- return False
+ start_iter.backward_chars(len(prefix))
+
+ assert self._contact is not None
+ client = app.get_client(self._contact.account)
+ client.get_module('Chatstate').block_chatstates(self._contact, True)
- if self._nick_hits:
- shell_like_completion = app.settings.get('shell_like_completion')
-
- if len(text_split) < 2 or with_refer_to_nick_char:
- # This is the 1st word of the line or no word or we are
- # cycling at the beginning, possibly with a space in one nick
- add = gc_refer_to_nick_char + ' '
- else:
- add = ' '
- start_iter = end_iter.copy()
- if (self._last_key_tab and
- with_refer_to_nick_char or (text and text[-1] == ' ')):
- # have to accommodate for the added space from last completion
- # gc_refer_to_nick_char may be more than one char!
- start_iter.backward_chars(len(begin) + len(add))
- elif self._last_key_tab and not shell_like_completion:
- # have to accommodate for the added space from last
- # completion
- start_iter.backward_chars(
- len(begin) + len(gc_refer_to_nick_char))
- else:
- start_iter.backward_chars(len(begin))
-
- assert self._account
- client = app.get_client(self._account)
- client.get_module('Chatstate').block_chatstates(
- self._contact, True)
-
- message_buffer.delete(start_iter, end_iter)
- # get a shell-like completion
- # if there's more than one nick for this completion, complete
- # only the part that all these nicks have in common
- if shell_like_completion and len(self._nick_hits) > 1:
- end = False
- completion = ''
- add = '' # if nick is not complete, don't add anything
- while not end and len(completion) < len(self._nick_hits[0]):
- completion = self._nick_hits[0][:len(completion) + 1]
- for nick in self._nick_hits:
- if completion.lower() not in nick.lower():
- end = True
- completion = completion[:-1]
- break
- # if the current nick matches a COMPLETE existing nick,
- # and if the user tab TWICE, complete that nick (with the
- # "add")
- if self._last_key_tab:
- for nick in self._nick_hits:
- if nick == completion:
- # The user seems to want this nick, so
- # complete it as if it were the only nick
- # available
- add = gc_refer_to_nick_char + ' '
- else:
- completion = self._nick_hits[0]
- message_buffer.insert_at_cursor(completion + add)
-
- client.get_module('Chatstate').block_chatstates(
- self._contact, False)
+ message_buffer.delete(start_iter, end_iter)
+ completion = self._suggestions[0]
+ message_buffer.insert_at_cursor(completion + suffix)
+
+ client.get_module('Chatstate').block_chatstates(self._contact, False)
self._last_key_tab = True
+
return True
- def _jid_not_blocked(self, resource: str) -> bool:
- assert self._account
- assert self._contact
- resource_contact = self._contact.get_resource(resource)
- return not jid_is_blocked(
- self._account, str(resource_contact.jid))
+ def _generate_suggestions(self, prefix: str) -> list[str]:
+ def _nick_matching(nick: str) -> bool:
+ assert self._contact is not None
+ if nick == self._contact.nickname:
+ return False
+
+ participant = self._contact.get_resource(nick)
+ if jid_is_blocked(self._contact.account, str(participant.jid)):
+ return False
+
+ if prefix == '':
+ return True
+
+ return nick.lower().startswith(prefix.lower())
+
+ assert self._contact is not None
+ # Get recent nicknames from DB. This enables us to suggest
+ # nicknames even if no message arrived since Gajim was started.
+ recent_nicknames = app.storage.archive.get_recent_muc_nicks(
+ self._contact)
+
+ matches: list[str] = []
+ for nick in recent_nicknames:
+ if _nick_matching(nick):
+ matches.append(nick)
+
+ # Add all other MUC participants
+ other_nicks: list[str] = []
+ for contact in self._contact.get_participants():
+ if _nick_matching(contact.name):
+ if contact.name not in matches:
+ other_nicks.append(contact.name)
+ other_nicks.sort(key=str.lower)
+
+ return matches + other_nicks
+
+ def _on_gc_message_received(self, event: GcMessageReceived) -> None:
+ if self._contact is None:
+ return
+
+ if event.room_jid != self._contact.jid:
+ return
+
+ if not self._last_key_tab:
+ # Clear suggestions if not actively using them
+ # (new messages may have new nicks)
+ self._suggestions.clear()