From 1a507966c61dbcfabe5786e60a8657665386226c Mon Sep 17 00:00:00 2001
From: Dominik George <dominik.george@teckids.org>
Date: Sun, 15 Jan 2023 01:00:56 +0100
Subject: [PATCH] Properly handle all offline and invisible states

---
 aleksis/core/assets/app/apollo.js             | 34 +++++++---
 .../core/assets/components/app/ping.graphql   |  4 +-
 aleksis/core/assets/index.js                  |  1 +
 aleksis/core/assets/mixins/offline.js         | 65 +++++++------------
 aleksis/core/schema/__init__.py               |  5 +-
 5 files changed, 56 insertions(+), 53 deletions(-)

diff --git a/aleksis/core/assets/app/apollo.js b/aleksis/core/assets/app/apollo.js
index 35b1b231d..7236b9d3f 100644
--- a/aleksis/core/assets/app/apollo.js
+++ b/aleksis/core/assets/app/apollo.js
@@ -4,7 +4,6 @@
 
 import { ApolloClient, HttpLink, from } from "@/apollo-boost";
 
-import { onError } from "@/apollo-link-error";
 import { RetryLink } from "@/apollo-link-retry";
 import { persistCache, LocalStorageWrapper } from "@/apollo3-cache-persist";
 import { InMemoryCache } from "@/apollo-cache-inmemory";
@@ -97,15 +96,6 @@ function getGraphqlURL() {
 const links = [
   // Automatically retry failed queries
   new RetryLink(),
-  // Add custom error handlers
-  onError(({ graphQLErrors, networkError }) => {
-    // Add a snackbar on all errors returned by the GraphQL endpoint
-    if (graphQLErrors) addErrorSnackbarItem("graphql.snackbar_error_message");
-    // Set app offline globally on network errors
-    //  This will cause the offline logic to kick in, starting a ping check or
-    //  similar recovery strategies depending on the app/navigator state
-    if (networkError) app.offline = true;
-  }),
   // Finally, the HTTP link to the real backend (Django)
   new HttpLink({
     uri: getGraphqlURL(),
@@ -122,6 +112,30 @@ const apolloClient = new ApolloClient({
 
 const apolloOpts = {
   defaultClient: apolloClient,
+  defaultOptions: {
+    $query: {
+      skip: (vm, queryKey) => {
+        // We only want to run this query when background activity is on and we are not reported offline
+        return !vm.$root.backgroundActive || vm.$root.offline;
+      },
+      error: ({ graphQLErrors, networkError }, vm, key, type, options) => {
+        if (graphQLErrors) {
+          // Add a snackbar on all errors returned by the GraphQL endpoint
+          console.error("A GraphQL query failed on the server");
+          addErrorSnackbarItem("graphql.snackbar_error_message");
+        }
+        if (networkError) {
+          // Set app offline globally on network errors
+          //  This will cause the offline logic to kick in, starting a ping check or
+          //  similar recovery strategies depending on the app/navigator state
+          console.error(
+            "Network error during GraphQL query, setting offline state"
+          );
+          vm.$root.offline = true;
+        }
+      },
+    },
+  },
 };
 
 export default apolloOpts;
diff --git a/aleksis/core/assets/components/app/ping.graphql b/aleksis/core/assets/components/app/ping.graphql
index 1a13edeb7..cc2f908e4 100644
--- a/aleksis/core/assets/components/app/ping.graphql
+++ b/aleksis/core/assets/components/app/ping.graphql
@@ -1,3 +1,3 @@
-{
-  ping
+query Pinf($payload: String) {
+  ping(payload: $payload)
 }
diff --git a/aleksis/core/assets/index.js b/aleksis/core/assets/index.js
index 459978ed1..ac9c36e45 100644
--- a/aleksis/core/assets/index.js
+++ b/aleksis/core/assets/index.js
@@ -46,6 +46,7 @@ const app = new Vue({
     showCacheAlert: false,
     contentLoading: false,
     offline: false,
+    backgroundActive: true,
   }),
   router,
   i18n,
diff --git a/aleksis/core/assets/mixins/offline.js b/aleksis/core/assets/mixins/offline.js
index 7bdd551a3..02a34644e 100644
--- a/aleksis/core/assets/mixins/offline.js
+++ b/aleksis/core/assets/mixins/offline.js
@@ -23,63 +23,48 @@ const offlineMixin = {
   },
   mounted() {
     window.addEventListener("online", (e) => {
-      console.debug("Navigator changed status to online.");
-      if (!document.hidden) this.toggleGlobalQueries(false, true);
+      console.info("Navigator changed status to online.");
+      this.checkOfflineState();
     });
     window.addEventListener("offline", (e) => {
-      console.debug("Navigator changed status to offline.");
-      this.toggleGlobalQueries(false, false);
+      console.info("Navigator changed status to offline.");
+      this.checkOfflineState();
     });
     document.addEventListener("visibilitychange", () => {
-      if (document.visibilityState === "hidden") {
-        console.debug("Visibility changed status to hidden.");
-        this.toggleGlobalQueries(false, false);
-      } else if (navigator.online) {
-        console.debug(
-          "Visibility changed status to visible. Navigator status is online."
-        );
-        this.toggleGlobalQueries(false, true);
-      }
+      console.info("Visibility changed status to", document.visibilityState);
+      this.checkOfflineState();
     });
   },
   methods: {
-    toggleGlobalQueries(enable, pingPolling) {
-      console.debug("Toggling global queries: " + (enable ? "on" : "off"));
-      if (enable) {
-        this.$apollo.queries.whoAmI.startPolling(10000);
-        this.$apollo.queries.messages.startPolling(1000);
-      } else {
-        this.$apollo.queries.whoAmI.stopPolling();
-        this.$apollo.queries.messages.stopPolling();
-      }
-      if (pingPolling) {
-        console.debug("Starting ping polling.");
-        this.$apollo.queries.ping.observer.resetLastResults();
-        this.ping = null;
-        this.$apollo.queries.ping.startPolling(1000);
+    checkOfflineState() {
+      if (navigator.onLine && document.visibilityState === "visible") {
+        console.info("Resuming background activity");
+        this.$root.backgroundActive = true;
       } else {
-        this.$apollo.queries.ping.stopPolling();
+        console.info("Pausing background activity");
+        this.$root.backgroundActive = false;
       }
     },
   },
   apollo: {
     ping: {
       query: gqlPing,
+      variables: () => {
+        return {
+          payload: Date.now().toString(),
+        };
+      },
+      pollInterval: 1000,
+      skip: (component, query) => {
+        // We only want to run this query when background activity is on and we are reported offline
+        return !(component.$root.backgroundActive && component.$root.offline);
+      },
     },
   },
   watch: {
-    ping: function (ping) {
-      if (ping === "pong") {
-        console.debug("Pong was received. Resuming regular queries.");
-        this.toggleGlobalQueries(true, false);
-        this.$root.offline = false;
-      }
-    },
-    "$root.offline": function (offline) {
-      if (navigator.onLine && !document.hidden) {
-        console.debug("Queries failed, but navigator is online.");
-        this.toggleGlobalQueries(false, true);
-      }
+    ping(payload) {
+      console.info("Pong received, clearing offline state");
+      this.$root.offline = false;
     },
   },
 };
diff --git a/aleksis/core/schema/__init__.py b/aleksis/core/schema/__init__.py
index a4ed81580..a94f88d48 100644
--- a/aleksis/core/schema/__init__.py
+++ b/aleksis/core/schema/__init__.py
@@ -27,7 +27,7 @@ from .user import UserType
 
 
 class Query(graphene.ObjectType):
-    ping = graphene.String(default_value="pong")
+    ping = graphene.String(payload=graphene.String())
 
     notifications = graphene.List(NotificationType)
 
@@ -53,6 +53,9 @@ class Query(graphene.ObjectType):
 
     custom_menu_by_name = graphene.Field(CustomMenuType, name=graphene.String())
 
+    def resolve_ping(root, info, payload) -> str:
+        return payload
+
     def resolve_notifications(root, info, **kwargs):
         return Notification.objects.filter(
             Q(
-- 
GitLab