From 1c49aef3a8fe714db68c4f91847f4a35e4399541 Mon Sep 17 00:00:00 2001 From: lovetox <philipp@hoerist.com> Date: Sun, 4 Apr 2021 22:55:03 +0200 Subject: [PATCH] Improve View sorting --- gajim/common/helpers.py | 7 + gajim/gtk/conversation/rows/base.py | 5 + gajim/gtk/conversation/rows/date.py | 6 +- gajim/gtk/conversation/rows/message.py | 17 ++ gajim/gtk/conversation/view.py | 250 ++++++++----------------- 5 files changed, 112 insertions(+), 173 deletions(-) diff --git a/gajim/common/helpers.py b/gajim/common/helpers.py index 5aa930f1d0..53c78a6a39 100644 --- a/gajim/common/helpers.py +++ b/gajim/common/helpers.py @@ -1360,3 +1360,10 @@ def get_muc_context(jid): if (disco_info.muc_is_members_only and disco_info.muc_is_nonanonymous): return 'private' return 'public' + + +def get_start_of_day(date_time): + return date_time.replace(hour=0, + minute=0, + second=0, + microsecond=0) diff --git a/gajim/gtk/conversation/rows/base.py b/gajim/gtk/conversation/rows/base.py index 96c3b4576f..55a043acda 100644 --- a/gajim/gtk/conversation/rows/base.py +++ b/gajim/gtk/conversation/rows/base.py @@ -39,6 +39,7 @@ def __init__(self, account, widget='label', history_mode=False): self.message_id = None self.log_line_id = None self.text = '' + self._merged = False self.get_style_context().add_class('conversation-row') @@ -58,6 +59,10 @@ def __init__(self, account, widget='label', history_mode=False): self.label.set_line_wrap_mode( Pango.WrapMode.WORD_CHAR) + @property + def is_merged(self): + return self._merged + def update_text_tags(self): if self.textview is not None: self.textview.update_text_tags() diff --git a/gajim/gtk/conversation/rows/date.py b/gajim/gtk/conversation/rows/date.py index faaff2ab65..a44b9713d0 100644 --- a/gajim/gtk/conversation/rows/date.py +++ b/gajim/gtk/conversation/rows/date.py @@ -19,7 +19,7 @@ class DateRow(BaseRow): - def __init__(self, account, date_string, timestamp): + def __init__(self, account, timestamp): BaseRow.__init__(self, account) self.set_selectable(False) @@ -29,8 +29,10 @@ def __init__(self, account, date_string, timestamp): self.timestamp = timestamp self.get_style_context().add_class('conversation-date-row') - self.label.set_text(date_string) + self.label.set_text(timestamp.strftime('%a, %d %b %Y')) self.label.set_halign(Gtk.Align.CENTER) self.label.set_hexpand(True) self.label.get_style_context().add_class('conversation-meta') self.grid.attach(self.label, 0, 0, 1, 1) + + self.show_all() diff --git a/gajim/gtk/conversation/rows/message.py b/gajim/gtk/conversation/rows/message.py index 667efee978..b23f57e796 100644 --- a/gajim/gtk/conversation/rows/message.py +++ b/gajim/gtk/conversation/rows/message.py @@ -13,6 +13,7 @@ # along with Gajim. If not, see <http://www.gnu.org/licenses/>. from datetime import datetime +from datetime import timedelta from gi.repository import Gdk from gi.repository import GLib @@ -31,6 +32,9 @@ from ...util import format_fingerprint +MERGE_TIMEFRAME = timedelta(seconds=120) + + class MessageRow(BaseRow): def __init__(self, account, @@ -135,6 +139,18 @@ def __init__(self, self.grid.attach(self._meta_box, 1, 0, 1, 1) self.grid.attach(bottom_box, 1, 1, 1, 1) + self.show_all() + + def is_same_sender(self, message): + return message.name == self.name + + def is_mergeable(self, message): + if message.type != self.type: + return False + if not self.is_same_sender(message): + return False + return abs(message.timestamp - self.timestamp) < MERGE_TIMEFRAME + def on_copy_message(self, _widget): timestamp = self.timestamp.strftime('%x, %X') clip = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) @@ -232,6 +248,7 @@ def update_avatar(self, avatar): self._avatar_surface.set_from_surface(avatar) def set_merged(self, merged): + self._merged = merged if merged: self.get_style_context().add_class('merged') self._avatar_surface.set_no_show_all(True) diff --git a/gajim/gtk/conversation/view.py b/gajim/gtk/conversation/view.py index 6ad58f154a..215f58d71c 100644 --- a/gajim/gtk/conversation/view.py +++ b/gajim/gtk/conversation/view.py @@ -28,6 +28,7 @@ from gajim.common import app from gajim.common.const import AvatarSize from gajim.common.helpers import to_user_string +from gajim.common.helpers import get_start_of_day from .util import scroll_to_end from .conversation.rows.read_marker import ReadMarkerRow @@ -53,6 +54,7 @@ class ConversationView(Gtk.ListBox): def __init__(self, account, contact, history_mode=False): Gtk.ListBox.__init__(self) self.set_selection_mode(Gtk.SelectionMode.NONE) + self.set_sort_func(self._sort_func) self._account = account self._client = app.get_client(account) self._contact = contact @@ -62,30 +64,15 @@ def __init__(self, account, contact, history_mode=False): self.autoscroll = True self.clearing = False - # Both first and last DateRow (datetime) - self._first_date = None - self._last_date = None - # Keeps track of the number of rows shown in ConversationView self._row_count = 0 self._max_row_count = 100 + self._active_date_rows = set() + # Keeps inserted message IDs to avoid re-inserting the same message self._message_ids_inserted = {} - # We keep a sorted array of all the timestamps we've inserted, which - # have a 1-to-1 mapping to the actual child elements of this ListBox - # (*all* rows are included). - # Binary-searching this array enables us to insert a message with a - # discontinuous timestamp (not less than or greater than the first or - # last message timestamp in the list), and insert it at the correct - # place in the ListBox. - self._timestamps_inserted = deque() - - # Stores the timestamp of the first *chat* row inserted - # (not a system row or date row) - self.first_message_timestamp = None - # Last incoming chat message timestamp (used for ReadMarkerRows) self._last_incoming_timestamp = datetime.fromtimestamp(0) @@ -93,7 +80,6 @@ def __init__(self, account, contact, history_mode=False): self._scroll_hint_row = ScrollHintRow(self._account, history_mode=self._history_mode) self.add(self._scroll_hint_row) - self._timestamps_inserted.append(datetime.fromtimestamp(0)) def clear(self): self.clearing = True @@ -112,15 +98,17 @@ def set_history_complete(self, complete): self._scroll_hint_row.set_history_complete(complete) def _reset_conversation_view(self): - self._first_date = None - self._last_date = None self._message_ids_inserted.clear() - self.first_message_timestamp = None self._last_incoming_timestamp = datetime.fromtimestamp(0) self._timestamps_inserted.clear() self._row_count = 0 self.clearing = False + def _sort_func(self, row1, row2): + if row1.timestamp == row2.timestamp: + return 0 + return -1 if row1.timestamp < row2.timestamp else 1 + def add_message(self, text, kind, @@ -201,7 +189,7 @@ def add_message(self, history_mode=self._history_mode, log_line_id=log_line_id) - self._insert_message(message, kind, history) + self._insert_message(message) def _get_avatar(self, kind, name): scale = self.get_scale_factor() @@ -217,128 +205,77 @@ def _get_avatar(self, kind, name): return contact.get_avatar(AvatarSize.ROSTER, scale, add_show=False) - def _insert_message(self, message, kind, history): - current_date = message.timestamp.strftime('%a, %d %b %Y') - - if self._is_out_of_order(message.timestamp, history): - insertion_point = bisect_left(self._timestamps_inserted, message.timestamp) - date_check_point = min(len( - self._timestamps_inserted) - 1, insertion_point - 1) - date_at_dcp = self._timestamps_inserted[date_check_point].strftime( - '%a, %d %b %Y') - if date_at_dcp != current_date: - associated_timestamp = message.timestamp - timedelta( - hours=message.timestamp.hour, - minutes=message.timestamp.minute, - seconds=message.timestamp.second) - date_row = DateRow( - self._account, current_date, associated_timestamp) - self.insert(date_row, insertion_point) - self._timestamps_inserted.insert( - insertion_point, associated_timestamp) - self._row_count += 1 - insertion_point += 1 - if (kind in ('incoming', 'incoming_queue') and - message.timestamp > self._last_incoming_timestamp): - self._last_incoming_timestamp = message.timestamp - self.insert(message, insertion_point) - self._timestamps_inserted.insert(insertion_point, message.timestamp) - current_timestamp = message.timestamp - self._row_count += 1 - elif history: - if current_date != self._first_date: - associated_timestamp = message.timestamp - timedelta( - hours=message.timestamp.hour, - minutes=message.timestamp.minute, - seconds=message.timestamp.second) - date_row = DateRow( - self._account, current_date, associated_timestamp) - self.insert(date_row, 1) - self._timestamps_inserted.insert(1, associated_timestamp) - self._row_count += 1 - self._first_date = current_date - if kind in ('incoming', 'incoming_queue', 'outgoing'): - self.first_message_timestamp = message.timestamp - if (kind in ('incoming', 'incoming_queue') and - message.timestamp > self._last_incoming_timestamp): - self._last_incoming_timestamp = message.timestamp - self.insert(message, 2) - self._timestamps_inserted.insert(2, message.timestamp) - if self._last_date is None: - self._last_date = current_date - current_timestamp = message.timestamp - self._row_count += 1 - else: - if current_date != self._last_date: - associated_timestamp = message.timestamp - timedelta( - hours=message.timestamp.hour, - minutes=message.timestamp.minute, - seconds=message.timestamp.second) - date_row = DateRow( - self._account, current_date, associated_timestamp) - self.add(date_row) - self._timestamps_inserted.append(associated_timestamp) - self._row_count += 1 - if self._first_date is None: - self._first_date = current_date - if (kind in ('incoming', 'incoming_queue', 'outgoing') and not - self.first_message_timestamp): - self.first_message_timestamp = message.timestamp - if (kind in ('incoming', 'incoming_queue') and - message.timestamp > self._last_incoming_timestamp): - self._last_incoming_timestamp = message.timestamp - self._last_date = current_date - self.add(message) - self._timestamps_inserted.append(message.timestamp) - current_timestamp = message.timestamp - self._row_count += 1 - - if message.type == 'chat': - self._merge_message(current_timestamp) - self._update_read_marker(current_timestamp) - - self.show_all() - - def _is_out_of_order(self, time_: datetime, history: bool) -> bool: - if history: - if self.first_message_timestamp: - return time_ > self.first_message_timestamp - return False - if len(self._timestamps_inserted) > 1: - return time_ < self._timestamps_inserted[-1] - return False - - def _merge_message(self, timestamp): - # 'Merge' message rows if they both meet certain conditions - # (see _is_mergeable). A merged message row does not display any - # avatar or meta info, and makes it look merged with the previous row. - if self._contact.is_groupchat: + def _insert_message(self, message): + self.add(message) + self._add_date_row(message.timestamp) + self._check_for_merge(message) + + def _add_date_row(self, timestamp): + start_of_day = get_start_of_day(timestamp) + if start_of_day in self._active_date_rows: return - current_index = self._timestamps_inserted.index(timestamp) - previous_row = self.get_row_at_index(current_index - 1) - current_row = self.get_row_at_index(current_index) - next_row = self.get_row_at_index(current_index + 1) - if next_row is not None: - if self._is_mergeable(current_row, next_row): - next_row.set_merged(True) - - if self._is_mergeable(current_row, previous_row): - current_row.set_merged(True) - if next_row is not None: - if self._is_mergeable(current_row, next_row): - next_row.set_merged(True) - - @staticmethod - def _is_mergeable(row1, row2): - # TODO: Check for same encryption - timestamp1 = row1.timestamp.strftime('%H:%M') - timestamp2 = row2.timestamp.strftime('%H:%M') - kind1 = row1.kind - kind2 = row2.kind - if timestamp1 == timestamp2 and kind1 == kind2: - return True - return False + date_row = DateRow(self._account, start_of_day) + self._active_date_rows.add(start_of_day) + self.add(date_row) + + row = self.get_row_at_index(date_row.get_index() + 1) + if row is None: + return + + if row.type != 'chat': + return + + row.set_merged(False) + + def _check_for_merge(self, message): + if message.type != 'chat': + return + + ancestor = self._find_ancestor(message) + if ancestor is None: + self._update_descendants(message) + else: + if message.is_mergeable(ancestor): + message.set_merged(True) + + def _find_ancestor(self, message): + index = message.get_index() + while index != 0: + index -= 1 + row = self.get_row_at_index(index) + if row is None: + return None + + if row.type != 'chat': + return None + + if not message.is_same_sender(row): + return None + + if not row.is_merged: + return row + return None + + def _update_descendants(self, message): + index = message.get_index() + while True: + index += 1 + row = self.get_row_at_index(index) + if row is None: + return + + if row.type != 'chat': + return + + if message.is_mergeable(row): + row.set_merged(True) + continue + + if message.is_same_sender(row): + row.set_merged(False) + self._update_descendants(row) + return def reduce_message_count(self): successful = False @@ -352,8 +289,6 @@ def reduce_message_count(self): # it’s safe to remove the fist row self.remove(row1) successful = True - self._timestamps_inserted.remove(row1.timestamp) - self._first_date = row2.timestamp.strftime('%a, %d %b %Y') self._row_count -= 1 continue @@ -362,14 +297,6 @@ def reduce_message_count(self): # remove the second row instead self.remove(row2) successful = True - self._timestamps_inserted.remove(row2.timestamp) - if row2.message_id: - self._message_ids_inserted.pop(row2.message_id) - chat_row = self._get_first_chat_row() - if chat_row is not None: - self.first_message_timestamp = chat_row.timestamp - else: - self.first_message_timestamp = None self._row_count -= 1 continue @@ -377,17 +304,6 @@ def reduce_message_count(self): # Not a date row, safe to remove self.remove(row1) successful = True - self._timestamps_inserted.remove(row1.timestamp) - if row1.message_id: - self._message_ids_inserted.pop(row1.message_id) - if row2.type == 'chat': - self.first_message_timestamp = row2.timestamp - else: - chat_row = self._get_first_chat_row() - if chat_row is not None: - self.first_message_timestamp = chat_row.timestamp - else: - self.first_message_timestamp = None self._row_count -= 1 return successful @@ -408,12 +324,6 @@ def iter_rows(self): for row in self.get_children(): yield row - def _get_first_chat_row(self): - for row in self.get_children(): - if row.type == 'chat': - return row - return None - def set_read_marker(self, id_): message_row = self._get_row_by_message_id(id_) if message_row is None: @@ -516,5 +426,3 @@ def show_error(self, id_, error): def on_quote(self, text): self.emit('quote', text) - - # TODO: focus_out_line for group chats -- GitLab