# HG changeset patch # User Goffi # Date 1575635133 -3600 # Node ID d9d2b56f46db3977e2fdd95618afcc3befb3ff00 # Parent 5868a5575e01460bdade39e11d2bb9ae3d0874a1 plugin chat: infinite scroll: each when use scroll on top of current history, 30 new messages are prepended. diff -r 5868a5575e01 -r d9d2b56f46db cagou/plugins/plugin_wid_chat.kv --- 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 : @@ -98,11 +99,15 @@ : 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 diff -r 5868a5575e01 -r d9d2b56f46db cagou/plugins/plugin_wid_chat.py --- 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)