From e75092f816d330e184674f1cfcb77c9aab15fa2d Mon Sep 17 00:00:00 2001
From: lovetox <philipp@hoerist.com>
Date: Sat, 3 Apr 2021 22:48:50 +0200
Subject: [PATCH] Refactor scrolling

---
 gajim/chat_control_base.py             | 197 +++++++------------------
 gajim/common/storage/archive.py        |  17 +--
 gajim/data/gui/chat_control.ui         |   4 -
 gajim/data/gui/groupchat_control.ui    |   4 -
 gajim/gtk/conversation/rows/info.py    |   7 +-
 gajim/gtk/conversation/rows/message.py |   7 +-
 gajim/gtk/conversation/view.py         |  77 +++++-----
 7 files changed, 113 insertions(+), 200 deletions(-)

diff --git a/gajim/chat_control_base.py b/gajim/chat_control_base.py
index 00ebd77368..24072cf7ee 100644
--- a/gajim/chat_control_base.py
+++ b/gajim/chat_control_base.py
@@ -25,7 +25,6 @@
 
 import os
 import sys
-import datetime
 import time
 import uuid
 import tempfile
@@ -189,18 +188,16 @@ def __init__(self, parent_win, widget_name, jid, acct,
         self.xml.conversation_scrolledwindow.add(self.conversation_view)
         self._scrolled_old_upper = None
         self._scrolled_old_value = None
-        self._scrolled_previous_value = 0
 
-        self._fetching_history = False
-        self._initial_fetch_done = False
+        self._fetch_start_upper = None
+        self._current_upper = 0
+        self._autoscroll = True
+        self._no_more_messages = False
 
         vadjustment = self.xml.conversation_scrolledwindow.get_vadjustment()
-        vadjustment.connect(
-            'changed', self._on_conversation_vadjustment_changed)
-        vadjustment.connect('value-changed', self._on_scroll_value_changed)
-        vscrollbar = self.xml.conversation_scrolledwindow.get_vscrollbar()
-        vscrollbar.connect('button-release-event',
-                           self._on_scrollbar_button_release)
+
+        vadjustment.connect('notify::upper', self._on_adj_upper_changed)
+        vadjustment.connect('notify::value', self._on_adj_value_changed)
 
         self.msg_textview = MessageInputTextView()
         self.msg_textview.connect('paste-clipboard',
@@ -1413,32 +1410,30 @@ def _on_load_history(self, _button, lines):
         self.fetch_n_lines_history(lines)
 
     def fetch_n_lines_history(self, n_lines):
-        if self._fetching_history:
-            return
-
-        timestamp_end = self.conversation_view.first_message_timestamp
-        if not timestamp_end:
-            timestamp_end = datetime.datetime.now()
-        self._fetching_history = True
-        adjustment = self.xml.conversation_scrolledwindow.get_vadjustment()
-
-        # Store these values so we can restore scroll position later
-        self._scrolled_old_upper = adjustment.get_upper()
-        self._scrolled_old_value = adjustment.get_value()
+        row = self.conversation_view.get_first_message_row()
+        if row is None:
+            timestamp = time.time()
+        else:
+            timestamp = row.db_timestamp
 
         if self.is_groupchat:
             messages = app.storage.archive.get_conversation_muc_before(
                 self.account,
                 self.contact.jid,
-                timestamp_end,
+                timestamp,
                 n_lines)
         else:
             messages = app.storage.archive.get_conversation_before(
                 self.account,
                 self.contact.jid,
-                timestamp_end,
+                timestamp,
                 n_lines)
 
+        if not messages:
+            self._no_more_messages = True
+            print('SET NO MORE')
+            return
+
         for msg in messages:
             if not msg:
                 continue
@@ -1468,79 +1463,6 @@ def fetch_n_lines_history(self, n_lines):
                 error=msg.error,
                 history=True)
 
-    def _on_edge_reached(self, _scrolledwindow, pos):
-        if pos != Gtk.PositionType.BOTTOM:
-            return
-        # Remove all events and set autoscroll True
-        app.log('autoscroll').info('Autoscroll enabled')
-        self.conversation_view.autoscroll = True
-        if self.resource:
-            jid = self.contact.get_full_jid()
-        else:
-            jid = self.contact.jid
-        types_list = []
-        if self._type.is_groupchat:
-            types_list = ['printed_gc_msg', 'gc_msg', 'printed_marked_gc_msg']
-        else:
-            types_list = [f'printed_{self._type}', str(self._type)]
-
-        if not app.events.get_events(self.account, jid, types_list):
-            return
-        if not self.parent_win:
-            return
-        if (self.parent_win.get_active_control() == self and
-                self.parent_win.window.is_active()):
-            # we are at the end
-            if not app.events.remove_events(
-                    self.account, jid, types=types_list):
-                # There were events to remove
-                self.redraw_after_event_removed(jid)
-                # XEP-0333 Send <displayed> tag
-                self._client.get_module('ChatMarkers').send_displayed_marker(
-                    self.contact,
-                    self.last_msg_id,
-                    self._type)
-                self.last_msg_id = None
-
-    def _on_edge_overshot(self, _scrolledwindow, pos):
-        # Fetch messages if we scroll against the top
-        if pos != Gtk.PositionType.TOP:
-            return
-        self.fetch_n_lines_history(30)
-
-    def _on_scroll_value_changed(self, adjustment):
-        if self.conversation_view.clearing or self._fetching_history:
-            return
-
-        value = adjustment.get_value()
-        previous_value = self._scrolled_previous_value
-        self._scrolled_previous_value = value
-
-        if value >= previous_value:
-            return
-
-        # Fetch messages before we reach the top
-        if value <= int(0.1 * adjustment.get_upper()):
-            self.fetch_n_lines_history(10)
-
-    def _on_scroll_realize(self, _widget):
-        # Initial message fetching when ChatControl is first opened
-        if not self._initial_fetch_done:
-            self._initial_fetch_done = True
-            restore_lines = app.settings.get('restore_lines')
-            if restore_lines <= 0:
-                return
-            self.fetch_n_lines_history(restore_lines)
-
-    def _on_scrollbar_button_release(self, scrollbar, event):
-        if event.get_button()[1] != 1:
-            # We want only to catch the left mouse button
-            return
-
-        if not at_the_end(scrollbar.get_parent()):
-            app.log('autoscroll').info('Autoscroll disabled')
-            self.conversation_view.autoscroll = False
-
     def has_focus(self):
         if self.parent_win:
             if self.parent_win.window.get_property('has-toplevel-focus'):
@@ -1548,57 +1470,46 @@ def has_focus(self):
                     return True
         return False
 
-    def _on_scroll(self, widget, event):
-        if not self.conversation_view.autoscroll:
-            # autoscroll is already disabled
-            return
+    def _on_adj_upper_changed(self, adj, *args):
+        upper = adj.get_upper()
+        diff = upper - self._current_upper
 
-        if widget is None:
-            # call from _on_conversation_view_key_press()
-            # SHIFT + Gdk.KEY_Page_Up
-            if event != Gdk.KEY_Page_Up:
-                return
+        if diff != 0:
+            self._current_upper = upper
+            if self._autoscroll:
+                adj.set_value(adj.get_upper() - adj.get_page_size())
+            else:
+                # Workaround: https://gitlab.gnome.org/GNOME/gtk/merge_requests/395
+                self.xml.conversation_scrolledwindow.set_kinetic_scrolling(True)
+                adj.set_value(adj.get_value() + diff)
+
+        if upper == adj.get_page_size():
+            # There is no scrollbar, load history until there is
+            self.fetch_n_lines_history(30)
+
+    def _on_adj_value_changed(self, adj, *args):
+        bottom = adj.get_upper() - adj.get_page_size()
+        if (bottom - adj.get_value()) < 1:
+            self._autoscroll = True
         else:
-            # On scrolling UP disable autoscroll
-            # get_scroll_direction() sets has_direction only TRUE
-            # if smooth scrolling is deactivated. If we have smooth
-            # smooth scrolling we have to use get_scroll_deltas()
-            has_direction, direction = event.get_scroll_direction()
-            if not has_direction:
-                direction = None
-                smooth, delta_x, delta_y = event.get_scroll_deltas()
-                if smooth:
-                    if delta_y < 0:
-                        direction = Gdk.ScrollDirection.UP
-                    elif delta_y > 0:
-                        direction = Gdk.ScrollDirection.DOWN
-                    elif delta_x < 0:
-                        direction = Gdk.ScrollDirection.LEFT
-                    elif delta_x > 0:
-                        direction = Gdk.ScrollDirection.RIGHT
-                else:
-                    app.log('autoscroll').warning(
-                        'Scroll directions can’t be determined')
+            self._autoscroll = False
 
-            if direction != Gdk.ScrollDirection.UP:
-                return
-        # Check if we have a Scrollbar
-        adjustment = self.xml.conversation_scrolledwindow.get_vadjustment()
-        if adjustment.get_upper() != adjustment.get_page_size():
-            app.log('autoscroll').info('Autoscroll disabled')
-            self.conversation_view.autoscroll = False
-
-    def _on_conversation_vadjustment_changed(self, adjustment):
-        self.scroll_to_end()
-        if self._fetching_history:
-            # Make sure the scroll position is kept
-            new_upper = adjustment.get_upper()
-            diff = new_upper - self._scrolled_old_upper
-            new_value = diff + self._scrolled_old_value
-            adjustment.set_value(new_value)
-            self._fetching_history = False
+        if self._no_more_messages:
+            self._fetch_start_upper = None
             return
 
+        if self._fetch_start_upper == adj.get_upper():
+            return
+
+        self._fetch_start_upper = None
+
+        # Load messages when we are near the top
+        if adj.get_value() < adj.get_page_size() * 2:
+            self._fetch_start_upper = adj.get_upper()
+            # Workaround: https://gitlab.gnome.org/GNOME/gtk/merge_requests/395
+            self.xml.conversation_scrolledwindow.set_kinetic_scrolling(False)
+            self.fetch_n_lines_history(30)
+
     def scroll_messages(self, direction, msg_buf, msg_type):
         if msg_type == 'sent':
             history = self.sent_history
diff --git a/gajim/common/storage/archive.py b/gajim/common/storage/archive.py
index a1b618e4e0..f9a0bae50b 100644
--- a/gajim/common/storage/archive.py
+++ b/gajim/common/storage/archive.py
@@ -321,15 +321,15 @@ def convert_show_values_to_db_api_values(show):
         return None
 
     @timeit
-    def get_conversation_before(self, account, jid, end_timestamp, n_lines):
+    def get_conversation_before(self, account, jid, timestamp, n_lines):
         """
-        Load n_lines lines of conversation with jid before end_timestamp
+        Load n_lines lines of conversation with jid before timestamp
 
         :param account:         The account
 
         :param jid:             The jid for which we request the conversation
 
-        :param end_timestamp:   end timestamp / datetime.datetime instance
+        :param timestamp:       timestamp
 
         returns a list of namedtuples
         """
@@ -351,19 +351,18 @@ def get_conversation_before(self, account, jid, end_timestamp, n_lines):
 
         return self._con.execute(
             sql,
-            tuple(jids) + (end_timestamp.timestamp(), n_lines)).fetchall()
+            tuple(jids) + (timestamp, n_lines)).fetchall()
 
     @timeit
-    def get_conversation_muc_before(self, account, jid, end_timestamp,
-                                    n_lines):
+    def get_conversation_muc_before(self, account, jid, timestamp, n_lines):
         """
-        Load n_lines lines of conversation with jid before end_timestamp
+        Load n_lines lines of conversation with jid before timestamp
 
         :param account:         The account
 
         :param jid:             The jid for which we request the conversation
 
-        :param end_timestamp:   end timestamp / datetime.datetime instance
+        :param timestamp:       timestamp
 
         returns a list of namedtuples
         """
@@ -386,7 +385,7 @@ def get_conversation_muc_before(self, account, jid, end_timestamp,
 
         return self._con.execute(
             sql,
-            tuple(jids) + (end_timestamp.timestamp(), n_lines)).fetchall()
+            tuple(jids) + (timestamp, n_lines)).fetchall()
 
     @timeit
     def get_last_conversation_line(self, account, jid):
diff --git a/gajim/data/gui/chat_control.ui b/gajim/data/gui/chat_control.ui
index a402f01708..d64734e464 100644
--- a/gajim/data/gui/chat_control.ui
+++ b/gajim/data/gui/chat_control.ui
@@ -623,10 +623,6 @@
                     <property name="can_focus">True</property>
                     <property name="shadow_type">in</property>
                     <property name="overlay_scrolling">False</property>
-                    <signal name="edge-overshot" handler="_on_edge_overshot" swapped="no"/>
-                    <signal name="edge-reached" handler="_on_edge_reached" swapped="no"/>
-                    <signal name="realize" handler="_on_scroll_realize" swapped="no"/>
-                    <signal name="scroll-event" handler="_on_scroll" swapped="no"/>
                     <child>
                       <placeholder/>
                     </child>
diff --git a/gajim/data/gui/groupchat_control.ui b/gajim/data/gui/groupchat_control.ui
index 5e11b173c0..f55130f6d7 100644
--- a/gajim/data/gui/groupchat_control.ui
+++ b/gajim/data/gui/groupchat_control.ui
@@ -348,10 +348,6 @@
                         <property name="can_focus">True</property>
                         <property name="shadow_type">in</property>
                         <property name="overlay_scrolling">False</property>
-                        <signal name="edge-overshot" handler="_on_edge_overshot" swapped="no"/>
-                        <signal name="edge-reached" handler="_on_edge_reached" swapped="no"/>
-                        <signal name="realize" handler="_on_scroll_realize" swapped="no"/>
-                        <signal name="scroll-event" handler="_on_scroll" swapped="no"/>
                         <child>
                           <placeholder/>
                         </child>
diff --git a/gajim/gtk/conversation/rows/info.py b/gajim/gtk/conversation/rows/info.py
index 9835f5e671..2a0184cf61 100644
--- a/gajim/gtk/conversation/rows/info.py
+++ b/gajim/gtk/conversation/rows/info.py
@@ -12,6 +12,8 @@
 # You should have received a copy of the GNU General Public License
 # along with Gajim. If not, see <http://www.gnu.org/licenses/>.
 
+from datetime import datetime
+
 from gi.repository import GLib
 from gi.repository import Gtk
 
@@ -34,7 +36,8 @@ def __init__(self,
         BaseRow.__init__(self, account, widget='textview',
                          history_mode=history_mode)
         self.type = 'info'
-        self.timestamp = timestamp
+        self.timestamp = datetime.fromtimestamp(timestamp)
+        self.db_timestamp = timestamp
         self.kind = kind
 
         if subject:
@@ -50,7 +53,7 @@ def __init__(self,
         avatar_placeholder = Gtk.Box()
         avatar_placeholder.set_size_request(AvatarSize.ROSTER, -1)
         self.grid.attach(avatar_placeholder, 0, 0, 1, 2)
-        timestamp_widget = self.create_timestamp_widget(timestamp)
+        timestamp_widget = self.create_timestamp_widget(self.timestamp)
         timestamp_widget.set_valign(Gtk.Align.START)
         self.grid.attach(timestamp_widget, 2, 0, 1, 1)
 
diff --git a/gajim/gtk/conversation/rows/message.py b/gajim/gtk/conversation/rows/message.py
index 84363293aa..667efee978 100644
--- a/gajim/gtk/conversation/rows/message.py
+++ b/gajim/gtk/conversation/rows/message.py
@@ -12,6 +12,8 @@
 # You should have received a copy of the GNU General Public License
 # along with Gajim. If not, see <http://www.gnu.org/licenses/>.
 
+from datetime import datetime
+
 from gi.repository import Gdk
 from gi.repository import GLib
 from gi.repository import Gtk
@@ -57,7 +59,8 @@ def __init__(self,
         BaseRow.__init__(self, account, widget='textview',
                          history_mode=history_mode)
         self.type = 'chat'
-        self.timestamp = timestamp
+        self.timestamp = datetime.fromtimestamp(timestamp)
+        self.db_timestamp = timestamp
         self.message_id = message_id
         self.log_line_id = log_line_id
         self.kind = kind
@@ -84,7 +87,7 @@ def __init__(self,
         self._meta_box = Gtk.Box(spacing=6)
         self._meta_box.pack_start(
             self.create_name_widget(name, kind, is_groupchat), False, True, 0)
-        timestamp_label = self.create_timestamp_widget(timestamp)
+        timestamp_label = self.create_timestamp_widget(self.timestamp)
         timestamp_label.set_margin_start(6)
         self._meta_box.pack_end(timestamp_label, False, True, 0)
         # TODO: implement app.settings.get('print_time') 'always', 'sometimes'?
diff --git a/gajim/gtk/conversation/view.py b/gajim/gtk/conversation/view.py
index a20a3caf27..6382c688ff 100644
--- a/gajim/gtk/conversation/view.py
+++ b/gajim/gtk/conversation/view.py
@@ -105,6 +105,12 @@ def clear(self):
 
         GLib.idle_add(self._reset_conversation_view)
 
+    def get_first_message_row(self):
+        for row in self.get_children():
+            if isinstance(row, MessageRow):
+                return row
+        return None
+
     def _reset_conversation_view(self):
         self._first_date = None
         self._last_date = None
@@ -149,7 +155,6 @@ def add_message(self,
 
         if not timestamp:
             timestamp = time.time()
-        time_ = datetime.fromtimestamp(timestamp)
 
         if other_text_tags is None:
             other_text_tags = []
@@ -158,7 +163,7 @@ def add_message(self,
                 (subject and self._contact.is_groupchat)):
             message = InfoMessageRow(
                 self._account,
-                time_,
+                timestamp,
                 text,
                 other_text_tags,
                 kind,
@@ -181,7 +186,7 @@ def add_message(self,
             message = MessageRow(
                 self._account,
                 message_id,
-                time_,
+                timestamp,
                 kind,
                 name,
                 text,
@@ -196,11 +201,11 @@ def add_message(self,
                 history_mode=self._history_mode,
                 log_line_id=log_line_id)
 
-        self._insert_message(message, time_, kind, history)
+        self._insert_message(message, kind, history)
 
         # Check for maximum message count
-        if self.autoscroll and self._row_count > self._max_row_count:
-            self._reduce_message_count()
+        # if self.autoscroll and self._row_count > self._max_row_count:
+        #     self._reduce_message_count()
 
     def _get_avatar(self, kind, name):
         scale = self.get_scale_factor()
@@ -216,20 +221,20 @@ def _get_avatar(self, kind, name):
 
         return contact.get_avatar(AvatarSize.ROSTER, scale, add_show=False)
 
-    def _insert_message(self, message, time_, kind, history):
-        current_date = time_.strftime('%a, %d %b %Y')
+    def _insert_message(self, message, kind, history):
+        current_date = message.timestamp.strftime('%a, %d %b %Y')
 
-        if self._is_out_of_order(time_, history):
-            insertion_point = bisect_left(self._timestamps_inserted, time_)
+        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 = time_ - timedelta(
-                    hours=time_.hour,
-                    minutes=time_.minute,
-                    seconds=time_.second)
+                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)
@@ -238,18 +243,18 @@ def _insert_message(self, message, time_, kind, history):
                 self._row_count += 1
                 insertion_point += 1
             if (kind in ('incoming', 'incoming_queue') and
-                    time_ > self._last_incoming_timestamp):
-                self._last_incoming_timestamp = time_
+                    message.timestamp > self._last_incoming_timestamp):
+                self._last_incoming_timestamp = message.timestamp
             self.insert(message, insertion_point)
-            self._timestamps_inserted.insert(insertion_point, time_)
-            current_timestamp = time_
+            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 = time_ - timedelta(
-                    hours=time_.hour,
-                    minutes=time_.minute,
-                    seconds=time_.second)
+                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)
@@ -257,22 +262,22 @@ def _insert_message(self, message, time_, kind, history):
                 self._row_count += 1
             self._first_date = current_date
             if kind in ('incoming', 'incoming_queue', 'outgoing'):
-                self.first_message_timestamp = time_
+                self.first_message_timestamp = message.timestamp
             if (kind in ('incoming', 'incoming_queue') and
-                    time_ > self._last_incoming_timestamp):
-                self._last_incoming_timestamp = time_
+                    message.timestamp > self._last_incoming_timestamp):
+                self._last_incoming_timestamp = message.timestamp
             self.insert(message, 2)
-            self._timestamps_inserted.insert(2, time_)
+            self._timestamps_inserted.insert(2, message.timestamp)
             if self._last_date is None:
                 self._last_date = current_date
-            current_timestamp = time_
+            current_timestamp = message.timestamp
             self._row_count += 1
         else:
             if current_date != self._last_date:
-                associated_timestamp = time_ - timedelta(
-                    hours=time_.hour,
-                    minutes=time_.minute,
-                    seconds=time_.second)
+                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)
@@ -282,14 +287,14 @@ def _insert_message(self, message, time_, kind, history):
                 self._first_date = current_date
             if (kind in ('incoming', 'incoming_queue', 'outgoing') and not
                     self.first_message_timestamp):
-                self.first_message_timestamp = time_
+                self.first_message_timestamp = message.timestamp
             if (kind in ('incoming', 'incoming_queue') and
-                    time_ > self._last_incoming_timestamp):
-                self._last_incoming_timestamp = time_
+                    message.timestamp > self._last_incoming_timestamp):
+                self._last_incoming_timestamp = message.timestamp
             self._last_date = current_date
             self.add(message)
-            self._timestamps_inserted.append(time_)
-            current_timestamp = time_
+            self._timestamps_inserted.append(message.timestamp)
+            current_timestamp = message.timestamp
             self._row_count += 1
 
         if message.type == 'chat':
-- 
GitLab