From ca190251870c239d49c45d245a2a219fe4169625 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Tue, 4 Dec 2018 12:50:29 +0300
Subject: [PATCH 1/2] Added support for async following including delayed
 confirmation if we followed user or not

---
 .../user_card_content/user_card_content.js    | 46 +++++++++++++++++++
 .../user_card_content/user_card_content.vue   | 21 +++++++--
 src/i18n/en.json                              |  4 ++
 src/i18n/ru.json                              |  4 ++
 4 files changed, 71 insertions(+), 4 deletions(-)

diff --git a/src/components/user_card_content/user_card_content.js b/src/components/user_card_content/user_card_content.js
index b5dd9b91..235cfb49 100644
--- a/src/components/user_card_content/user_card_content.js
+++ b/src/components/user_card_content/user_card_content.js
@@ -5,6 +5,8 @@ export default {
   props: [ 'user', 'switcher', 'selected', 'hideBio' ],
   data () {
     return {
+      followRequestInProgress: false,
+      followRequestSent: false,
       hideUserStatsLocal: typeof this.$store.state.config.hideUserStats === 'undefined'
         ? this.$store.state.instance.hideUserStats
         : this.$store.state.config.hideUserStats
@@ -70,13 +72,57 @@ export default {
   methods: {
     followUser () {
       const store = this.$store
+      this.followRequestInProgress = true
       store.state.api.backendInteractor.followUser(this.user.id)
         .then((followedUser) => store.commit('addNewUsers', [followedUser]))
+        .then(() => {
+          if (this.user.following) {
+            this.followRequestInProgress = false
+            return
+          }
+          if (!this.user.locked) {
+            let attemptsLeft = 3
+            const fetchUser = () => new Promise((resolve, reject) => {
+              setTimeout(() => {
+                store.state.api.backendInteractor.fetchUser({ id: this.user.id })
+                  .then((user) => store.commit('addNewUsers', [user]))
+                  .then(() => resolve(this.user.following))
+                  .catch((e) => reject(e))
+              }, 500)
+            }).then((confirmed) => {
+              if (!confirmed && attemptsLeft > 0) {
+                attemptsLeft--
+                return fetchUser()
+              } else if (confirmed) {
+                return true
+              } else {
+                return false
+              }
+            })
+
+            return fetchUser()
+              .then((successfulConfirmation) => {
+                if (successfulConfirmation) {
+                  this.followRequestInProgress = false
+                } else {
+                  this.followRequestInProgress = false
+                  this.followRequestSent = true
+                }
+              })
+          } else {
+            this.followRequestInProgress = false
+            this.followRequestSent = true
+          }
+        })
     },
     unfollowUser () {
       const store = this.$store
+      this.followRequestInProgress = true
       store.state.api.backendInteractor.unfollowUser(this.user.id)
         .then((unfollowedUser) => store.commit('addNewUsers', [unfollowedUser]))
+        .then(() => {
+          this.followRequestInProgress = false
+        })
     },
     blockUser () {
       const store = this.$store
diff --git a/src/components/user_card_content/user_card_content.vue b/src/components/user_card_content/user_card_content.vue
index 84669d7f..1dcf348f 100644
--- a/src/components/user_card_content/user_card_content.vue
+++ b/src/components/user_card_content/user_card_content.vue
@@ -44,13 +44,26 @@
           <div class="follow" v-if="loggedIn">
             <span v-if="user.following">
               <!--Following them!-->
-              <button @click="unfollowUser" class="pressed">
-                {{ $t('user_card.following') }}
+              <button @click="unfollowUser" class="pressed" :disabled="followRequestInProgress" :title="$t('user_card.follow_unfollow')">
+                <template v-if="followRequestInProgress">
+                  {{ $t('user_card.follow_progress') }}
+                </template>
+                <template v-else>
+                  {{ $t('user_card.following') }}
+                </template>
               </button>
             </span>
             <span v-if="!user.following">
-              <button @click="followUser">
-                {{ $t('user_card.follow') }}
+              <button @click="followUser" :disabled="followRequestInProgress" :title="followRequestSent ? $t('user_card.follow_again') : ''">
+                <template v-if="followRequestInProgress">
+                  {{ $t('user_card.follow_progress') }}
+                </template>
+                <template v-else-if="followRequestSent">
+                  {{ $t('user_card.follow_sent') }}
+                </template>
+                <template v-else>
+                  {{ $t('user_card.follow') }}
+                </template>
               </button>
             </span>
           </div>
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 893db931..9d12f09e 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -193,6 +193,10 @@
     "blocked": "Blocked!",
     "deny": "Deny",
     "follow": "Follow",
+    "follow_sent": "Request sent!",
+    "follow_progress": "Requesting…",
+    "follow_again": "Send request again?",
+    "follow_unfollow": "Stop following",
     "followees": "Following",
     "followers": "Followers",
     "following": "Following!",
diff --git a/src/i18n/ru.json b/src/i18n/ru.json
index 9c28ccf4..37d88e3c 100644
--- a/src/i18n/ru.json
+++ b/src/i18n/ru.json
@@ -156,6 +156,10 @@
     "block": "Заблокировать",
     "blocked": "Заблокирован",
     "follow": "Читать",
+    "follow_sent": "Запрос отправлен!",
+    "follow_progress": "Запрашиваем…",
+    "follow_again": "Запросить еще заново?",
+    "follow_unfollow": "Перестать читать",
     "followees": "Читаемые",
     "followers": "Читатели",
     "following": "Читаю",

From 13fbef4550e0e95024df9f1cc77764b8c734ca3f Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Fri, 14 Dec 2018 17:33:48 +0300
Subject: [PATCH 2/2] slightly changed the code to have less mutations, added
 comments

---
 .../user_card_content/user_card_content.js    | 71 +++++++++++--------
 1 file changed, 41 insertions(+), 30 deletions(-)

diff --git a/src/components/user_card_content/user_card_content.js b/src/components/user_card_content/user_card_content.js
index 8c6de8ec..206dba88 100644
--- a/src/components/user_card_content/user_card_content.js
+++ b/src/components/user_card_content/user_card_content.js
@@ -90,43 +90,54 @@ export default {
       store.state.api.backendInteractor.followUser(this.user.id)
         .then((followedUser) => store.commit('addNewUsers', [followedUser]))
         .then(() => {
+          // For locked users we just mark it that we sent the follow request
+          if (this.user.locked) {
+            this.followRequestInProgress = false
+            this.followRequestSent = true
+            return
+          }
+
           if (this.user.following) {
+            // If we get result immediately, just stop.
             this.followRequestInProgress = false
             return
           }
-          if (!this.user.locked) {
-            let attemptsLeft = 3
-            const fetchUser = () => new Promise((resolve, reject) => {
-              setTimeout(() => {
-                store.state.api.backendInteractor.fetchUser({ id: this.user.id })
-                  .then((user) => store.commit('addNewUsers', [user]))
-                  .then(() => resolve(this.user.following))
-                  .catch((e) => reject(e))
-              }, 500)
-            }).then((confirmed) => {
-              if (!confirmed && attemptsLeft > 0) {
-                attemptsLeft--
-                return fetchUser()
-              } else if (confirmed) {
-                return true
+
+          // But usually we don't get result immediately, so we ask server
+          // for updated user profile to confirm if we are following them
+          // Sometimes it takes several tries. Sometimes we end up not following
+          // user anyway, probably because they locked themselves and we
+          // don't know that yet.
+          // Recursive Promise, it will call itself up to 3 times.
+          const fetchUser = (attempt) => new Promise((resolve, reject) => {
+            setTimeout(() => {
+              store.state.api.backendInteractor.fetchUser({ id: this.user.id })
+                .then((user) => store.commit('addNewUsers', [user]))
+                .then(() => resolve([this.user.following, attempt]))
+                .catch((e) => reject(e))
+            }, 500)
+          }).then(([following, attempt]) => {
+            if (!following && attempt <= 3) {
+              // If we BE reports that we still not following that user - retry,
+              // increment attempts by one
+              return fetchUser(++attempt)
+            } else {
+              // If we run out of attempts, just return whatever status is.
+              return following
+            }
+          })
+
+          return fetchUser(1)
+            .then((following) => {
+              if (following) {
+                // We confirmed and everything its good.
+                this.followRequestInProgress = false
               } else {
-                return false
+                // If after all the tries, just treat it as if user is locked
+                this.followRequestInProgress = false
+                this.followRequestSent = true
               }
             })
-
-            return fetchUser()
-              .then((successfulConfirmation) => {
-                if (successfulConfirmation) {
-                  this.followRequestInProgress = false
-                } else {
-                  this.followRequestInProgress = false
-                  this.followRequestSent = true
-                }
-              })
-          } else {
-            this.followRequestInProgress = false
-            this.followRequestSent = true
-          }
         })
     },
     unfollowUser () {