Skip to content

Resolve "Infinite scrolling of Thread message feed"

Tomas Vik requested to merge 2245-tmf-infinite-scrolling into develop

Part of #2245 (closed)

What has been done

  1. the thread messages API endpoint accepts beforeId, afterId and limit arguments.
  2. Vuex actions and state to support infinite scrolling
  3. Adding slimmed down version of vue-intersect and using it to recognise user reaching either end of the TMF
  4. styling of TMF supports long lists
  5. TMF is always scrolled all the way down to the last message
  6. Replacing done from action test with async/await and adding an argument to mock action responses

What doesn't work

  1. fetching aroundId messages, if the permalink isn't within the last 50 messages, it will just show as the first message without its context
  2. links at the top and at the bottom of the list that provide an alternative to intersection observer

Testing strategy

  1. change the fetch limit to a smaller amount (e.g. 15). (alternatively, create 50+ messages)
  2. open TMF with more than 15 messages (notice that it automatically scrolls all the way down)
  3. scroll up and notice that older messages are fetched and displayed
  4. validate that after the whole history is fetched, no further requests to the API are being made

Webpack/babel issues faced in this MR

Webpack issue with vue-intersect

Adding vue-intersect (following diff) causes vue-ssr-renderer to fail on sytax error (not knowing import syntax).

diff --git a/package-lock.json b/package-lock.json
index 3f14d13ff..f4992db55 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -33128,6 +33128,11 @@
       "integrity": "sha512-KmvZVtmM26BQOMK1rwUZsrqxEGeKiYSZGA7SNWE6uExx8UX/cj9hq2MRV/wWC3Cq6AoeDGk57rL9YMFRel/q+g==",
       "dev": true
     },
+    "vue-intersect": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/vue-intersect/-/vue-intersect-1.1.3.tgz",
+      "integrity": "sha512-8ry42W6zHrpCpWtIiCMsYVBmEdQf9upAZnIFh8ffpCFbfhQcOOtX2xpwyzPaVAoW2MSZuSDGjaKK6Cy1z+Ap8w=="
+    },
     "vue-jest": {
       "version": "4.0.0-beta.2",
       "resolved": "https://registry.npmjs.org/vue-jest/-/vue-jest-4.0.0-beta.2.tgz",
diff --git a/package.json b/package.json
index fb9713417..e655985ae 100644
--- a/package.json
+++ b/package.json
@@ -178,6 +178,7 @@
     "useragent": "2.3.0",
     "uuid": "^3.3.2",
     "vue": "^2.6.8",
+    "vue-intersect": "^1.1.3",
     "vue-server-renderer": "^2.6.8",
     "vue-template-compiler": "^2.6.8",
     "vuedraggable": "^2.21.0",
diff --git a/public/js/vue/thread-message-feed/components/index.vue b/public/js/vue/thread-message-feed/components/index.vue
index 5f1744975..0934e756d 100644
--- a/public/js/vue/thread-message-feed/components/index.vue
+++ b/public/js/vue/thread-message-feed/components/index.vue
@@ -4,6 +4,7 @@ import ThreadHeader from './thread-header.vue';
 import ChatInput from './chat-input.vue';
 import ChatItem from './chat-item.vue';
 import LoadingSpinner from '../../components/loading-spinner.vue';
+import Intersect from 'vue-intersect';
 
 export default {
   name: 'ThreadMessageFeed',
@@ -11,8 +12,12 @@ export default {
     ChatInput,
     ThreadHeader,
     LoadingSpinner,
-    ChatItem
+    ChatItem,
+    Intersect
   },
+  data: () => ({
+    msg: ''
+  }),
   computed: {
     ...mapGetters({
       parentMessage: 'threadMessageFeed/parentMessage',
@@ -44,13 +49,17 @@ export default {
           <div v-else-if="childMessagesRequest.loading" class="loading-message">
             Loading thread <loading-spinner />
           </div>
-          <chat-item
-            v-for="message in childMessages"
-            v-else
-            :key="message.id"
-            :message="message"
-            :use-compact-styles="true"
-          />
+          <div v-else>
+            <intersect @enter="msg = 'Intersected'" @leave="msg = 'Not intersected'">
+              <div>{{ msg }}</div>
+            </intersect>
+            <chat-item
+              v-for="message in childMessages"
+              :key="message.id"
+              :message="message"
+              :use-compact-styles="true"
+            />
+          </div>
         </div>
         <chat-input :user="user" thread />
       </section>

Async/Await in Vuex

Using async/await in Vuex causes ssr-renderer to throw following error:

(node:24041) UnhandledPromiseRejectionWarning: ReferenceError: regeneratorRuntime is not defined
    at server-bundle.js:6895:7
    at Module.<anonymous> (public/js/vue/thread-message-feed/store/index.js:130:24)
    at Module../public/js/vue/thread-message-feed/store/index.js (server-bundle.js:6937:30)
    at __webpack_require__ (webpack/bootstrap:19:0)
    at Module../public/js/vue/store/index.js (server-bundle.js:5749:84)
    at __webpack_require__ (webpack/bootstrap:19:0)
    at Module../public/js/vue/entry-server.js (public/js/vue/entry-server.js:1:0)
    at __webpack_require__ (webpack/bootstrap:19:0)
    at server-bundle.js:85:18
    at Object.<anonymous> (server-bundle.js:88:10)
    at evaluateModule (/Users/tomas/workspace/gitter/webapp/node_modules/vue-server-renderer/build.dev.js:9274:21)
    at /Users/tomas/workspace/gitter/webapp/node_modules/vue-server-renderer/build.dev.js:9332:18
    at new Promise (<anonymous>)
    at /Users/tomas/workspace/gitter/webapp/node_modules/vue-server-renderer/build.dev.js:9324:14
    at Object.renderToString (/Users/tomas/workspace/gitter/webapp/node_modules/vue-server-renderer/build.dev.js:9500:9)
    at renderToString (/Users/tomas/workspace/gitter/webapp/server/handlers/renderers/vue-ssr-renderer.js:107:19)

This can be fixed by using @babel/plugin-transform-runtime as a babel plugin (right now we only use it in tests). But then the frontend code starts failing (because babel stops understanding CommonJS modules in favour of ES modules).

diff --git a/babel.config.js b/babel.config.js
index a65a78459..eef8c5c1a 100644
--- a/babel.config.js
+++ b/babel.config.js
@@ -1,7 +1,8 @@
 'use strict';

 const presets = ['@babel/preset-env'];
-const plugins = [];
+// needed for async/await in Vuex store
+const plugins = ['@babel/plugin-transform-runtime'];

 // Jest is running in node environment, so we need additional plugins
 const isJest = !!process.env.JEST_WORKER_ID;
@@ -11,11 +12,5 @@ if (isJest) {

 module.exports = {
   plugins,
-  presets,
-  env: {
-    test: {
-      // from https://github.com/facebook/jest/issues/3126#issuecomment-465926747
-      plugins: ['@babel/plugin-transform-runtime']
-    }
-  }
+  presets
 };

The browser errors caused by the change:

Screenshot_2019-09-13_at_08.42.46

Which is a predicted consequence of using the @babel/plugin-transform-runtime and having CommonJS modules.

After spending hours on tweaking webpack and babel and researching this issue, I'm opting in for the 2 following workarounds:

  • using promises in Vuex store
  • Copying code from vue-intersect into our codebase
Edited by Tomas Vik

Merge request reports