diff --git a/gajim/chat_control_base.py b/gajim/chat_control_base.py
index 9fd609bb1de48f1caf392a86eb6e14398df0b07a..600e1595b7ff52d101a95cddcf5fda3cccf8c55d 100644
--- a/gajim/chat_control_base.py
+++ b/gajim/chat_control_base.py
@@ -305,13 +305,9 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
             'conversation_scrolledwindow')
         self.conv_scrolledwindow.add(self.conv_textview.tv)
         widget = self.conv_scrolledwindow.get_vadjustment()
-        id_ = widget.connect('value-changed',
-            self.on_conversation_vadjustment_value_changed)
-        self.handlers[id_] = widget
         id_ = widget.connect('changed',
             self.on_conversation_vadjustment_changed)
         self.handlers[id_] = widget
-        self.was_at_the_end = True
         self.correcting = False
         self.last_sent_msg = None
 
@@ -965,7 +961,7 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
         full_jid = self.get_full_jid()
         textview = self.conv_textview
         end = False
-        if self.was_at_the_end or kind == 'outgoing':
+        if self.conv_textview.autoscroll or kind == 'outgoing':
             end = True
 
         if other_tags_for_name is None:
@@ -1201,7 +1197,7 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
         if state:
             self.set_emoticon_popover()
             jid = self.contact.jid
-            if self.was_at_the_end:
+            if self.conv_textview.autoscroll:
                 # we are at the end
                 type_ = ['printed_' + self.type_id]
                 if self.type_id == message_control.TYPE_GC:
@@ -1221,21 +1217,14 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
         else:
             self.send_chatstate('inactive', self.contact)
 
-    def scroll_to_end_iter(self):
-        self.conv_textview.scroll_to_end_iter()
-        return False
+    def scroll_to_end(self, force=False):
+        self.conv_textview.scroll_to_end(force)
 
-    def on_conversation_vadjustment_changed(self, adjustment):
-        # used to stay at the end of the textview when we shrink conversation
-        # textview.
-        if self.was_at_the_end:
-            self.scroll_to_end_iter()
-        self.was_at_the_end = (adjustment.get_upper() - adjustment.get_value()\
-            - adjustment.get_page_size()) < 18
-
-    def on_conversation_vadjustment_value_changed(self, adjustment):
-        self.was_at_the_end = (adjustment.get_upper() - adjustment.get_value() \
-            - adjustment.get_page_size()) < 18
+    def _on_edge_reached(self, scrolledwindow, pos):
+        if pos != Gtk.PositionType.BOTTOM:
+            return
+        # Remove all events and set autoscroll True
+        self.conv_textview.autoscroll = True
         if self.resource:
             jid = self.contact.get_full_jid()
         else:
@@ -1252,8 +1241,7 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
             return
         if not self.parent_win:
             return
-        if self.conv_textview.at_the_end() and \
-        self.parent_win.get_active_control() == self and \
+        if self.parent_win.get_active_control() == self and \
         self.parent_win.window.is_active():
             # we are at the end
             if self.type_id == message_control.TYPE_GC:
@@ -1264,6 +1252,18 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
                 # There were events to remove
                 self.redraw_after_event_removed(jid)
 
+    def _on_scroll(self, widget, event):
+        # On scrolliung UP disable autoscroll
+        has_direction, direction = event.get_scroll_direction()
+        if has_direction and direction == Gdk.ScrollDirection.UP:
+            # Check if we have a Scrollbar
+            adjustment = self.conv_scrolledwindow.get_vadjustment()
+            if adjustment.get_upper() != adjustment.get_page_size():
+                self.conv_textview.autoscroll = False
+
+    def on_conversation_vadjustment_changed(self, adjustment):
+        self.scroll_to_end()
+
     def redraw_after_event_removed(self, jid):
         """
         We just removed a 'printed_*' event, redraw contact in roster or
@@ -1344,7 +1344,7 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
         """
         # make the last message visible, when changing to "full view"
         if not state:
-            GLib.idle_add(self.conv_textview.scroll_to_end_iter)
+            self.scroll_to_end()
 
         widget.set_no_show_all(state)
         if state:
diff --git a/gajim/conversation_textview.py b/gajim/conversation_textview.py
index 8ab318afa8dde6fec91de9ab16635c06630362b6..3129d88d348d027c9d889fa402713d45b925a177 100644
--- a/gajim/conversation_textview.py
+++ b/gajim/conversation_textview.py
@@ -206,7 +206,7 @@ class ConversationTextview(GObject.GObject):
         self.last_sent_message_id = None
         # last_received_message_id[name] = (msg_stanza_id, line_start_mark)
         self.last_received_message_id = {}
-
+        self.autoscroll = True
         # connect signals
         id_ = self.tv.connect('populate_popup', self.on_textview_populate_popup)
         self.handlers[id_] = self.tv
@@ -370,16 +370,9 @@ class ConversationTextview(GObject.GObject):
         self.tv.tagXMPP.set_property('foreground', color)
         self.tv.tagSthAtSth.set_property('foreground', color)
 
-    def at_the_end(self):
-        return gtkgui_helpers.at_the_end(self.tv.get_parent())
-
-    def scroll_to_end_iter(self):
-        buffer_ = self.tv.get_buffer()
-        end_iter = buffer_.get_end_iter()
-        if not end_iter:
-            return False
-        self.tv.scroll_to_iter(end_iter, 0, False, 1, 1)
-        return False # when called in an idle_add, just do it once
+    def scroll_to_end(self, force=False):
+        if self.autoscroll or force:
+            gtkgui_helpers.scroll_to_end(self.tv.get_parent())
 
     def correct_message(self, correct_id, kind, name):
         allowed = True
@@ -449,7 +442,7 @@ class ConversationTextview(GObject.GObject):
         buffer_.end_user_action()
         del self.xep0184_marks[id_]
 
-    def show_focus_out_line(self, scroll=True):
+    def show_focus_out_line(self):
         if not self.allow_focus_out_line:
             # if room did not receive focus-in from the last time we added
             # --- line then do not readd
@@ -505,11 +498,7 @@ class ConversationTextview(GObject.GObject):
                     buffer_.get_end_iter(), left_gravity=True)
 
             buffer_.end_user_action()
-
-            if scroll:
-                # scroll to the end (via idle in case the scrollbar has
-                # appeared)
-                GLib.idle_add(self.scroll_to_end_iter)
+            self.scroll_to_end()
 
     def clear(self, tv = None):
         """
@@ -1231,9 +1220,9 @@ class ConversationTextview(GObject.GObject):
             self.last_sent_message_id = (msg_stanza_id, new_mark)
 
         if not insert_mark:
-            if self.at_the_end() or kind == 'outgoing':
+            if self.autoscroll or kind == 'outgoing':
                 # we are at the end or we are sending something
-                GLib.idle_add(self.scroll_to_end_iter)
+                self.scroll_to_end(force=True)
 
         self.just_cleared = False
         buffer_.end_user_action()
diff --git a/gajim/data/gui/chat_control.ui b/gajim/data/gui/chat_control.ui
index 0b7ac417c1222db4643ed9c2444ee4efaaca01f2..eb36a218d3aa964ced522d26e061703208d51037 100644
--- a/gajim/data/gui/chat_control.ui
+++ b/gajim/data/gui/chat_control.ui
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<!-- Generated with glade 3.20.2 -->
+<!-- Generated with glade 3.21.0 -->
 <interface>
   <requires lib="gtk+" version="3.20"/>
   <object class="GtkAdjustment" id="adjustment1">
@@ -594,6 +594,8 @@
                 <property name="can_focus">True</property>
                 <property name="shadow_type">in</property>
                 <property name="overlay_scrolling">False</property>
+                <signal name="edge-reached" handler="_on_edge_reached" 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 9e15ec0145d7a88c9784f2c235ff05b5a75296bb..6c37ae8f6c5790901c39b25b76bc3a14db5d4c15 100644
--- a/gajim/data/gui/groupchat_control.ui
+++ b/gajim/data/gui/groupchat_control.ui
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<!-- Generated with glade 3.20.2 -->
+<!-- Generated with glade 3.21.0 -->
 <interface>
   <requires lib="gtk+" version="3.20"/>
   <object class="GtkMenu" id="formattings_menu">
@@ -187,6 +187,8 @@
                     <property name="can_focus">True</property>
                     <property name="shadow_type">in</property>
                     <property name="overlay_scrolling">False</property>
+                    <signal name="edge-reached" handler="_on_edge_reached" swapped="no"/>
+                    <signal name="scroll-event" handler="_on_scroll" swapped="no"/>
                     <child>
                       <placeholder/>
                     </child>
diff --git a/gajim/groupchat_control.py b/gajim/groupchat_control.py
index c16020a55326a379086deced30aa4617856ff840..99c7912f3444cdcdc0a940239be20ebb09479c97 100644
--- a/gajim/groupchat_control.py
+++ b/gajim/groupchat_control.py
@@ -1380,8 +1380,7 @@ class GroupchatControl(ChatControlBase):
             # we have full focus (we are reading it!)
             return
 
-        at_the_end = self.conv_textview.at_the_end()
-        self.conv_textview.show_focus_out_line(scroll=at_the_end)
+        self.conv_textview.show_focus_out_line()
 
     def needs_visual_notification(self, text):
         """
diff --git a/gajim/gui_interface.py b/gajim/gui_interface.py
index 6ca8f423a55b2dcd75c131bbf4859cc950156d18..70d7acfd2299f994ef380c42cb2002692fab6fe4 100644
--- a/gajim/gui_interface.py
+++ b/gajim/gui_interface.py
@@ -1814,8 +1814,8 @@ class Interface:
             w.window.present()
             # Using isinstance here because we want to catch all derived types
             if isinstance(ctrl, ChatControlBase):
-                tv = ctrl.conv_textview
-                tv.scroll_to_end_iter()
+                ctrl.scroll_to_end()
+
 
     def join_gc_minimal(self, account, room_jid, password=None):
         if account is not None:
diff --git a/gajim/message_window.py b/gajim/message_window.py
index 99746185bd328e13f003a0b019fe712e4e9061a7..dad1a676a3d611583e8814d021c8d43a23199f40 100644
--- a/gajim/message_window.py
+++ b/gajim/message_window.py
@@ -292,16 +292,10 @@ class MessageWindow(object):
         self._controls[control.account][fjid] = control
 
         if self.get_num_controls() == 2:
-            # is first conversation_textview scrolled down ?
-            scrolled = False
             first_widget = self.notebook.get_nth_page(0)
             ctrl = self._widget_to_control(first_widget)
-            conv_textview = ctrl.conv_textview
-            if conv_textview.at_the_end():
-                scrolled = True
             self.notebook.set_show_tabs(True)
-            if scrolled:
-                GLib.idle_add(conv_textview.scroll_to_end_iter)
+            ctrl.scroll_to_end()
 
         # Add notebook page and connect up to the tab's close button
         xml = gtkgui_helpers.get_gtk_builder('message_window.ui', 'chat_tab_ebox')
diff --git a/gajim/roster_window.py b/gajim/roster_window.py
index 05bf7eaf405caf83ceaf4e27ea5b08c4119c5bb1..85cc20a6990abe909d85d3919b051568e5e8c1be 100644
--- a/gajim/roster_window.py
+++ b/gajim/roster_window.py
@@ -2723,7 +2723,7 @@ class RosterWindow:
                 xep0184_id=xep0184_id, additional_data=obj.additional_data)
             if obj.msg_log_id:
                 pw = obj.session.control.parent_win
-                end = obj.session.control.was_at_the_end
+                end = obj.session.control.conv_textview.autoscroll
                 if not pw or (pw.get_active_control() and obj.session.control \
                 == pw.get_active_control() and pw.is_active() and end):
                     app.logger.set_read_messages([obj.msg_log_id])