changeset 326:d9d2b56f46db

plugin chat: infinite scroll: each when use scroll on top of current history, 30 new messages are prepended.
author Goffi <goffi@goffi.org>
date Fri, 06 Dec 2019 13:25:33 +0100
parents 5868a5575e01
children b77792cc6d12
files cagou/plugins/plugin_wid_chat.kv cagou/plugins/plugin_wid_chat.py
diffstat 2 files changed, 110 insertions(+), 2 deletions(-) [+]
line wrap: on
line diff
--- a/cagou/plugins/plugin_wid_chat.kv	Fri Dec 06 13:25:31 2019 +0100
+++ b/cagou/plugins/plugin_wid_chat.kv	Fri Dec 06 13:25:33 2019 +0100
@@ -19,6 +19,7 @@
 #:import escape kivy.utils.escape_markup
 #:import SimpleXHTMLWidget cagou.core.simple_xhtml.SimpleXHTMLWidget
 #:import DelayedBoxLayout cagou.core.common_widgets.DelayedBoxLayout
+#:import ScrollEffect kivy.effects.scroll.ScrollEffect
 
 
 <MessAvatar>:
@@ -98,11 +99,15 @@
 <Chat>:
     message_input: message_input
     messages_widget: messages_widget
+    history_scroll: history_scroll
     ScrollView:
+        id: history_scroll
         scroll_y: 0
+        on_scroll_y: root.onScroll(*args)
         do_scroll_x: False
         scroll_type: ['bars', 'content']
-        bar_width: dp(6)
+        bar_width: dp(10)
+        effect_cls: ScrollEffect
         DelayedBoxLayout:
             id: messages_widget
             size_hint_y: None
--- a/cagou/plugins/plugin_wid_chat.py	Fri Dec 06 13:25:31 2019 +0100
+++ b/cagou/plugins/plugin_wid_chat.py	Fri Dec 06 13:25:33 2019 +0100
@@ -40,7 +40,6 @@
 from cagou.core.image import Image
 from cagou.core.common import SymbolButton, JidButton
 from cagou.core import menu
-# from random import randrange
 
 log = logging.getLogger(__name__)
 
@@ -67,6 +66,9 @@
 COLOR_ENCRYPTED = (0.4, 0.4, 0.4, 1)
 COLOR_ENCRYPTED_TRUSTED = (0.29,0.87,0.0,1)
 
+# below this limit, new messages will be prepended
+INFINITE_SCROLL_LIMIT = dp(600)
+
 
 class MessAvatar(Image):
     pass
@@ -401,6 +403,7 @@
 class Chat(quick_chat.QuickChat, cagou_widget.CagouWidget):
     message_input = properties.ObjectProperty()
     messages_widget = properties.ObjectProperty()
+    history_scroll = properties.ObjectProperty()
 
     def __init__(self, host, target, type_=C.CHAT_ONE2ONE, nick=None, occupants=None,
                  subject=None, profiles=None):
@@ -423,7 +426,9 @@
         extra_btn = ExtraButton(chat=self)
         self.headerInputAddExtra(extra_btn)
         self.header_input.hint_text = target
+        self._history_prepend_lock = False
         Clock.schedule_once(lambda dt: self.postInit(), 0)
+        self.history_count = 0
 
     def __str__(self):
         return "Chat({})".format(self.target)
@@ -565,6 +570,15 @@
         self.messages_widget.add_widget(MessageWidget(mess_data=mess_data))
         self.notify(mess_data)
 
+    def prependMessage(self, mess_data):
+        """Prepend a message Widget to the history
+
+        @param mess_data(quick_chat.Message): message data
+        """
+        mess_wid = self.messages_widget
+        last_idx = len(mess_wid.children)
+        mess_wid.add_widget(MessageWidget(mess_data=mess_data), index=last_idx)
+
     def _get_notif_msg(self, mess_data):
         return _("{nick}: {message}").format(
             nick=mess_data.nick,
@@ -742,6 +756,95 @@
         else:
             return False
 
+    def _history_unlock(self, __):
+        self._history_prepend_lock = False
+        log.debug("history prepend unlocked")
+        # we call manually onScroll, to check if we are still in the scrolling zone
+        self.onScroll(self.history_scroll, self.history_scroll.scroll_y)
+
+    def _history_scroll_adjust(self, __, scroll_start_height):
+        # history scroll position must correspond to where it was before new messages
+        # have been appended
+        self.history_scroll.scroll_y = (
+            scroll_start_height / self.messages_widget.height
+        )
+
+        # we want a small delay before unlocking, to avoid re-fetching history
+        # again
+        Clock.schedule_once(self._history_unlock, 1.5)
+
+    def _backHistoryGetCb_post(self, __, history, scroll_start_height):
+        if len(history) == 0:
+            # we don't unlock self._history_prepend_lock if there is no history, as there
+            # is no sense to try to retrieve more in this case.
+            log.debug(f"we've reached top of history for {self.target.bare} chat")
+        else:
+            # we have to schedule again for _history_scroll_adjust, else messages_widget
+            # is not resized (self.messages_widget.height is not yet updated)
+            # as a result, the scroll_to can't work correctly
+            Clock.schedule_once(partial(
+                self._history_scroll_adjust,
+                scroll_start_height=scroll_start_height))
+            log.debug(
+                f"{len(history)} messages prepended to history (last: {history[0][0]})")
+
+    def _backHistoryGetCb(self, history):
+        # TODO: factorise with QuickChat._historyGetCb
+        scroll_start_height = self.messages_widget.height * self.history_scroll.scroll_y
+        for data in reversed(history):
+            uid, timestamp, from_jid, to_jid, message, subject, type_, extra = data
+            from_jid = jid.JID(from_jid)
+            to_jid = jid.JID(to_jid)
+            extra["history"] = True
+            self.messages[uid] = message = quick_chat.Message(
+                self,
+                uid,
+                timestamp,
+                from_jid,
+                to_jid,
+                message,
+                subject,
+                type_,
+                extra,
+                self.profile,
+            )
+            self.messages.move_to_end(uid, last=False)
+            self.prependMessage(message)
+        Clock.schedule_once(partial(
+            self._backHistoryGetCb_post,
+            history=history,
+            scroll_start_height=scroll_start_height))
+
+    def _backHistoryGetEb(self, failure_):
+        G.host.addNote(
+            _("Problem while getting back history"),
+            _("Can't back history for {target}: {problem}").format(
+                target=self.target, problem=failure_),
+            C.XMLUI_DATA_LVL_ERROR)
+        # we don't unlock self._history_prepend_lock on purpose, no need
+        # to try to get more history if something is wrong
+
+    def onScroll(self, scroll_view, scroll_y):
+        if self._history_prepend_lock:
+            return
+        if (1-scroll_y) * self.messages_widget.height < INFINITE_SCROLL_LIMIT:
+            self._history_prepend_lock = True
+            log.debug(f"Retrieving back history for {self} [{self.history_count}]")
+            self.history_count += 1
+            first_uid = next(iter(self.messages.keys()))
+            filters = self.history_filters.copy()
+            filters['before_uid'] = first_uid
+            self.host.bridge.historyGet(
+                str(self.host.profiles[self.profile].whoami.bare),
+                str(self.target),
+                30,
+                True,
+                {k: str(v) for k,v in filters.items()},
+                self.profile,
+                callback=self._backHistoryGetCb,
+                errback=self._backHistoryGetEb,
+            )
+
 
 PLUGIN_INFO["factory"] = Chat.factory
 quick_widgets.register(quick_chat.QuickChat, Chat)