From e20a7be3aa4973400cd670347ff038a78eb12e4e Mon Sep 17 00:00:00 2001
From: dave <starpumadev@gmail.com>
Date: Wed, 20 Feb 2019 10:13:28 -0500
Subject: #376: update status timeline when it's empty

---
 src/components/user_profile/user_profile.vue | 1 +
 1 file changed, 1 insertion(+)

(limited to 'src/components/user_profile/user_profile.vue')

diff --git a/src/components/user_profile/user_profile.vue b/src/components/user_profile/user_profile.vue
index 79461291..09fb93de 100644
--- a/src/components/user_profile/user_profile.vue
+++ b/src/components/user_profile/user_profile.vue
@@ -10,6 +10,7 @@
       <Timeline
         :label="$t('user_card.statuses')"
         :disabled="!user.statuses_count"
+        :count="user.statuses_count"
         :embedded="true"
         :title="$t('user_profile.timeline_title')"
         :timeline="timeline"
-- 
cgit v1.2.3-70-g09d2


From e687b58091bcedb6f3a56d94030fa312e51830d5 Mon Sep 17 00:00:00 2001
From: taehoon <th.dev91@gmail.com>
Date: Thu, 21 Feb 2019 13:32:47 -0500
Subject: Show error message when visit profile page of invalid user

---
 src/components/user_profile/user_profile.js  | 8 ++++++++
 src/components/user_profile/user_profile.vue | 3 ++-
 src/i18n/en.json                             | 3 ++-
 src/modules/users.js                         | 2 +-
 src/services/api/api.service.js              | 6 ++++++
 5 files changed, 19 insertions(+), 3 deletions(-)

(limited to 'src/components/user_profile/user_profile.vue')

diff --git a/src/components/user_profile/user_profile.js b/src/components/user_profile/user_profile.js
index 37179ce1..44192e9a 100644
--- a/src/components/user_profile/user_profile.js
+++ b/src/components/user_profile/user_profile.js
@@ -4,6 +4,11 @@ import Timeline from '../timeline/timeline.vue'
 import FollowList from '../follow_list/follow_list.vue'
 
 const UserProfile = {
+  data () {
+    return {
+      error: false
+    }
+  },
   created () {
     this.$store.commit('clearTimeline', { timeline: 'user' })
     this.$store.commit('clearTimeline', { timeline: 'favorites' })
@@ -13,6 +18,9 @@ const UserProfile = {
     this.startFetchFavorites()
     if (!this.user.id) {
       this.$store.dispatch('fetchUser', this.fetchBy)
+        .catch(() => {
+          this.error = true
+        })
     }
   },
   destroyed () {
diff --git a/src/components/user_profile/user_profile.vue b/src/components/user_profile/user_profile.vue
index 09fb93de..ccebe20b 100644
--- a/src/components/user_profile/user_profile.vue
+++ b/src/components/user_profile/user_profile.vue
@@ -55,7 +55,8 @@
       </div>
     </div>
     <div class="panel-body">
-      <i class="icon-spin3 animate-spin"></i>
+      <span v-if="error">{{ $t('user_profile.profile_does_not_exist') }}</span>
+      <i class="icon-spin3 animate-spin" v-else></i>
     </div>
   </div>
 </div>
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 64753f1d..c482ecb6 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -383,7 +383,8 @@
     "mute_progress": "Muting..."
   },
   "user_profile": {
-    "timeline_title": "User Timeline"
+    "timeline_title": "User Timeline",
+    "profile_does_not_exist": "Sorry, this profile does not exist."
   },
   "who_to_follow": {
     "more": "More",
diff --git a/src/modules/users.js b/src/modules/users.js
index 77df7168..eabfe5ae 100644
--- a/src/modules/users.js
+++ b/src/modules/users.js
@@ -140,7 +140,7 @@ const users = {
   getters,
   actions: {
     fetchUser (store, id) {
-      store.rootState.api.backendInteractor.fetchUser({ id })
+      return store.rootState.api.backendInteractor.fetchUser({ id })
         .then((user) => store.commit('addNewUsers', [user]))
     },
     fetchBlocks (store) {
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index 3d2e8823..d8716596 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -244,6 +244,12 @@ const denyUser = ({id, credentials}) => {
 const fetchUser = ({id, credentials}) => {
   let url = `${USER_URL}?user_id=${id}`
   return fetch(url, { headers: authHeaders(credentials) })
+    .then((data) => {
+      if (!data.ok) {
+        throw Error(data.statusText)
+      }
+      return data
+    })
     .then((data) => data.json())
     .then((data) => parseUser(data))
 }
-- 
cgit v1.2.3-70-g09d2


From b78227456ea6b1a80cd85988d3ef91cb654a881c Mon Sep 17 00:00:00 2001
From: taehoon <th.dev91@gmail.com>
Date: Tue, 26 Feb 2019 12:26:04 -0500
Subject: Better error handling

---
 src/components/user_profile/user_profile.js  | 12 ++++++++++--
 src/components/user_profile/user_profile.vue |  2 +-
 src/i18n/en.json                             |  3 ++-
 src/services/api/api.service.js              | 15 +++++++++------
 src/services/errors/errors.js                | 14 ++++++++++++++
 5 files changed, 36 insertions(+), 10 deletions(-)
 create mode 100644 src/services/errors/errors.js

(limited to 'src/components/user_profile/user_profile.vue')

diff --git a/src/components/user_profile/user_profile.js b/src/components/user_profile/user_profile.js
index 44192e9a..ebf6c61a 100644
--- a/src/components/user_profile/user_profile.js
+++ b/src/components/user_profile/user_profile.js
@@ -1,3 +1,4 @@
+import get from 'lodash/get'
 import UserCardContent from '../user_card_content/user_card_content.vue'
 import UserCard from '../user_card/user_card.vue'
 import Timeline from '../timeline/timeline.vue'
@@ -18,8 +19,15 @@ const UserProfile = {
     this.startFetchFavorites()
     if (!this.user.id) {
       this.$store.dispatch('fetchUser', this.fetchBy)
-        .catch(() => {
-          this.error = true
+        .catch((reason) => {
+          const errorMessage = get(reason, 'error.error')
+          if (errorMessage === 'No user with such user_id') { // Known error
+            this.error = this.$t('user_profile.profile_does_not_exist')
+          } else if (errorMessage) {
+            this.error = errorMessage
+          } else {
+            this.error = this.$t('user_profile.profile_loading_error')
+          }
         })
     }
   },
diff --git a/src/components/user_profile/user_profile.vue b/src/components/user_profile/user_profile.vue
index ccebe20b..ba1a7760 100644
--- a/src/components/user_profile/user_profile.vue
+++ b/src/components/user_profile/user_profile.vue
@@ -55,7 +55,7 @@
       </div>
     </div>
     <div class="panel-body">
-      <span v-if="error">{{ $t('user_profile.profile_does_not_exist') }}</span>
+      <span v-if="error">{{ error }}</span>
       <i class="icon-spin3 animate-spin" v-else></i>
     </div>
   </div>
diff --git a/src/i18n/en.json b/src/i18n/en.json
index c482ecb6..9fdd1692 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -384,7 +384,8 @@
   },
   "user_profile": {
     "timeline_title": "User Timeline",
-    "profile_does_not_exist": "Sorry, this profile does not exist."
+    "profile_does_not_exist": "Sorry, this profile does not exist.",
+    "profile_loading_error": "Sorry, there was an error loading this profile."
   },
   "who_to_follow": {
     "more": "More",
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index d8716596..fbe5afa0 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -47,6 +47,7 @@ const MASTODON_USER_FAVORITES_TIMELINE_URL = '/api/v1/favourites'
 import { each, map } from 'lodash'
 import { parseStatus, parseUser, parseNotification } from '../entity_normalizer/entity_normalizer.service.js'
 import 'whatwg-fetch'
+import { StatusCodeError } from '../errors/errors'
 
 const oldfetch = window.fetch
 
@@ -244,13 +245,15 @@ const denyUser = ({id, credentials}) => {
 const fetchUser = ({id, credentials}) => {
   let url = `${USER_URL}?user_id=${id}`
   return fetch(url, { headers: authHeaders(credentials) })
-    .then((data) => {
-      if (!data.ok) {
-        throw Error(data.statusText)
-      }
-      return data
+    .then((response) => {
+      return new Promise((resolve, reject) => response.json()
+        .then((json) => {
+          if (!response.ok) {
+            return reject(new StatusCodeError(response.status, json, { url }, response))
+          }
+          return resolve(json)
+        }))
     })
-    .then((data) => data.json())
     .then((data) => parseUser(data))
 }
 
diff --git a/src/services/errors/errors.js b/src/services/errors/errors.js
new file mode 100644
index 00000000..548f3c68
--- /dev/null
+++ b/src/services/errors/errors.js
@@ -0,0 +1,14 @@
+export function StatusCodeError (statusCode, body, options, response) {
+  this.name = 'StatusCodeError'
+  this.statusCode = statusCode
+  this.message = statusCode + ' - ' + (JSON && JSON.stringify ? JSON.stringify(body) : body)
+  this.error = body // legacy attribute
+  this.options = options
+  this.response = response
+
+  if (Error.captureStackTrace) { // required for non-V8 environments
+    Error.captureStackTrace(this)
+  }
+}
+StatusCodeError.prototype = Object.create(Error.prototype)
+StatusCodeError.prototype.constructor = StatusCodeError
-- 
cgit v1.2.3-70-g09d2


From 080786c9458ba8b9db1ea63732824a3e297e10dc Mon Sep 17 00:00:00 2001
From: taehoon <th.dev91@gmail.com>
Date: Mon, 25 Feb 2019 04:51:23 -0500
Subject: Rewrite FollowList using hocs

---
 src/components/follow_list/follow_list.js    | 68 ----------------------------
 src/components/follow_list/follow_list.vue   | 33 --------------
 src/components/user_profile/user_profile.js  | 29 +++++++++++-
 src/components/user_profile/user_profile.vue | 10 +---
 src/hocs/with_load_more/with_load_more.js    |  2 +
 src/modules/users.js                         | 19 ++++++--
 6 files changed, 45 insertions(+), 116 deletions(-)
 delete mode 100644 src/components/follow_list/follow_list.js
 delete mode 100644 src/components/follow_list/follow_list.vue

(limited to 'src/components/user_profile/user_profile.vue')

diff --git a/src/components/follow_list/follow_list.js b/src/components/follow_list/follow_list.js
deleted file mode 100644
index 9777c87e..00000000
--- a/src/components/follow_list/follow_list.js
+++ /dev/null
@@ -1,68 +0,0 @@
-import UserCard from '../user_card/user_card.vue'
-
-const FollowList = {
-  data () {
-    return {
-      loading: false,
-      bottomedOut: false,
-      error: false
-    }
-  },
-  props: ['userId', 'showFollowers'],
-  created () {
-    window.addEventListener('scroll', this.scrollLoad)
-    if (this.entries.length === 0) {
-      this.fetchEntries()
-    }
-  },
-  destroyed () {
-    window.removeEventListener('scroll', this.scrollLoad)
-    this.$store.dispatch('clearFriendsAndFollowers', this.userId)
-  },
-  computed: {
-    user () {
-      return this.$store.getters.userById(this.userId)
-    },
-    entries () {
-      return this.showFollowers ? this.user.followers : this.user.friends
-    },
-    showFollowsYou () {
-      return !this.showFollowers || (this.showFollowers && this.userId !== this.$store.state.users.currentUser.id)
-    }
-  },
-  methods: {
-    fetchEntries () {
-      if (!this.loading) {
-        const command = this.showFollowers ? 'addFollowers' : 'addFriends'
-        this.loading = true
-        this.$store.dispatch(command, this.userId).then(entries => {
-          this.error = false
-          this.loading = false
-          this.bottomedOut = entries.length === 0
-        }).catch(() => {
-          this.error = true
-          this.loading = false
-        })
-      }
-    },
-    scrollLoad (e) {
-      const bodyBRect = document.body.getBoundingClientRect()
-      const height = Math.max(bodyBRect.height, -(bodyBRect.y))
-      if (this.loading === false &&
-        this.bottomedOut === false &&
-        this.$el.offsetHeight > 0 &&
-        (window.innerHeight + window.pageYOffset) >= (height - 750)
-      ) {
-        this.fetchEntries()
-      }
-    }
-  },
-  watch: {
-    'user': 'fetchEntries'
-  },
-  components: {
-    UserCard
-  }
-}
-
-export default FollowList
diff --git a/src/components/follow_list/follow_list.vue b/src/components/follow_list/follow_list.vue
deleted file mode 100644
index 27102edf..00000000
--- a/src/components/follow_list/follow_list.vue
+++ /dev/null
@@ -1,33 +0,0 @@
-<template>
-  <div class="follow-list">
-    <user-card
-      v-for="entry in entries"
-      :key="entry.id" :user="entry"
-      :noFollowsYou="!showFollowsYou"
-    />
-    <div class="text-center panel-footer">
-      <a v-if="error" @click="fetchEntries" class="alert error">
-        {{$t('general.generic_error')}}
-      </a>
-      <i v-else-if="loading" class="icon-spin3 animate-spin"/>
-      <span v-else-if="bottomedOut"></span>
-      <a v-else @click="fetchEntries">{{$t('general.more')}}</a>
-    </div>
-  </div>
-</template>
-
-<script src="./follow_list.js"></script>
-
-<style lang="scss">
-
-.follow-list {
-  .panel-footer {
-    padding: 10px;
-  }
-
-  .error {
-    font-size: 14px;
-  }
-}
-
-</style>
diff --git a/src/components/user_profile/user_profile.js b/src/components/user_profile/user_profile.js
index ebf6c61a..9f9d43e4 100644
--- a/src/components/user_profile/user_profile.js
+++ b/src/components/user_profile/user_profile.js
@@ -1,8 +1,32 @@
+import { compose } from 'vue-compose'
 import get from 'lodash/get'
 import UserCardContent from '../user_card_content/user_card_content.vue'
 import UserCard from '../user_card/user_card.vue'
 import Timeline from '../timeline/timeline.vue'
-import FollowList from '../follow_list/follow_list.vue'
+import withLoadMore from '../../hocs/with_load_more/with_load_more'
+import withList from '../../hocs/with_list/with_list'
+
+const FollowerList = compose(
+  withLoadMore({
+    fetch: (props, $store) => $store.dispatch('addFollowers', props.userId),
+    select: (props, $store) => get($store.getters.userById(props.userId), 'followers', []),
+    destory: (props, $store) => $store.dispatch('clearFollowers', props.userId),
+    childPropName: 'entries',
+    additionalPropNames: ['userId']
+  }),
+  withList({ getEntryProps: user => ({ user }) })
+)(UserCard)
+
+const FriendList = compose(
+  withLoadMore({
+    fetch: (props, $store) => $store.dispatch('addFriends', props.userId),
+    select: (props, $store) => get($store.getters.userById(props.userId), 'friends', []),
+    destory: (props, $store) => $store.dispatch('clearFriends', props.userId),
+    childPropName: 'entries',
+    additionalPropNames: ['userId']
+  }),
+  withList({ getEntryProps: user => ({ user }) })
+)(UserCard)
 
 const UserProfile = {
   data () {
@@ -123,7 +147,8 @@ const UserProfile = {
     UserCardContent,
     UserCard,
     Timeline,
-    FollowList
+    FollowerList,
+    FriendList
   }
 }
 
diff --git a/src/components/user_profile/user_profile.vue b/src/components/user_profile/user_profile.vue
index ba1a7760..a3d2825f 100644
--- a/src/components/user_profile/user_profile.vue
+++ b/src/components/user_profile/user_profile.vue
@@ -18,16 +18,10 @@
         :user-id="fetchBy"
       />
       <div :label="$t('user_card.followees')" v-if="followsTabVisible" :disabled="!user.friends_count">
-        <FollowList v-if="user.friends_count > 0" :userId="userId" :showFollowers="false" />
-        <div class="userlist-placeholder" v-else>
-          <i class="icon-spin3 animate-spin"></i>
-        </div>
+        <FriendList :userId="userId" />
       </div>
       <div :label="$t('user_card.followers')" v-if="followersTabVisible" :disabled="!user.followers_count">
-        <FollowList v-if="user.followers_count > 0" :userId="userId" :showFollowers="true" />
-        <div class="userlist-placeholder" v-else>
-          <i class="icon-spin3 animate-spin"></i>
-        </div>
+        <FollowerList :userId="userId" :entryProps="{noFollowsYou: isUs}" />
       </div>
       <Timeline
         :label="$t('user_card.media')"
diff --git a/src/hocs/with_load_more/with_load_more.js b/src/hocs/with_load_more/with_load_more.js
index 7d53e7ac..b8246ec9 100644
--- a/src/hocs/with_load_more/with_load_more.js
+++ b/src/hocs/with_load_more/with_load_more.js
@@ -7,6 +7,7 @@ import './with_load_more.scss'
 const withLoadMore = ({
   fetch,                      // function to fetch entries and return a promise
   select,                     // function to select data from store
+  destroy,                    // function called at "destroyed" lifecycle
   childPropName = 'entries',  // name of the prop to be passed into the wrapped component
   additionalPropNames = []    // additional prop name list of the wrapper component
 }) => (WrappedComponent) => {
@@ -58,6 +59,7 @@ const withLoadMore = ({
     },
     destroyed () {
       window.removeEventListener('scroll', this.scrollLoad)
+      destroy && destroy(this.$props, this.$store)
     },
     methods: {
       fetchEntries () {
diff --git a/src/modules/users.js b/src/modules/users.js
index b2821a92..093af497 100644
--- a/src/modules/users.js
+++ b/src/modules/users.js
@@ -72,14 +72,20 @@ export const mutations = {
   },
   // Because frontend doesn't have a reason to keep these stuff in memory
   // outside of viewing someones user profile.
-  clearFriendsAndFollowers (state, userKey) {
-    const user = state.usersObject[userKey]
+  clearFriends (state, userId) {
+    const user = state.usersObject[userId]
     if (!user) {
       return
     }
     user.friends = []
-    user.followers = []
     user.friendsPage = 0
+  },
+  clearFollowers (state, userId) {
+    const user = state.usersObject[userId]
+    if (!user) {
+      return
+    }
+    user.followers = []
     user.followersPage = 0
   },
   addNewUsers (state, users) {
@@ -197,8 +203,11 @@ const users = {
           return followers
         })
     },
-    clearFriendsAndFollowers ({ commit }, userKey) {
-      commit('clearFriendsAndFollowers', userKey)
+    clearFriends ({ commit }, userId) {
+      commit('clearFriends', userId)
+    },
+    clearFollowers ({ commit }, userId) {
+      commit('clearFollowers', userId)
     },
     registerPushNotifications (store) {
       const token = store.state.currentUser.credentials
-- 
cgit v1.2.3-70-g09d2


From 3d30ad1dda8f31960586625bb6a432d6b3adde8e Mon Sep 17 00:00:00 2001
From: dave <starpumadev@gmail.com>
Date: Sun, 3 Mar 2019 12:53:01 -0500
Subject: #417: refresh tab on user profile only

---
 src/components/tab_switcher/tab_switcher.js  | 6 ++++--
 src/components/user_profile/user_profile.vue | 2 +-
 2 files changed, 5 insertions(+), 3 deletions(-)

(limited to 'src/components/user_profile/user_profile.vue')

diff --git a/src/components/tab_switcher/tab_switcher.js b/src/components/tab_switcher/tab_switcher.js
index 1de936e8..03da8249 100644
--- a/src/components/tab_switcher/tab_switcher.js
+++ b/src/components/tab_switcher/tab_switcher.js
@@ -4,7 +4,7 @@ import './tab_switcher.scss'
 
 export default Vue.component('tab-switcher', {
   name: 'TabSwitcher',
-  props: ['renderOnlyFocused'],
+  props: ['refresh', 'renderOnlyFocused'],
   data () {
     return {
       active: this.$slots.default.findIndex(_ => _.tag)
@@ -12,7 +12,9 @@ export default Vue.component('tab-switcher', {
   },
   watch: {
     $route () {
-      this.activateTab(0)
+      if (this.refresh) {
+        this.active = 0
+      }
     }
   },
   methods: {
diff --git a/src/components/user_profile/user_profile.vue b/src/components/user_profile/user_profile.vue
index a3d2825f..54f1b97b 100644
--- a/src/components/user_profile/user_profile.vue
+++ b/src/components/user_profile/user_profile.vue
@@ -6,7 +6,7 @@
       :switcher="true"
       :selected="timeline.viewing"
     />
-    <tab-switcher :renderOnlyFocused="true">
+    <tab-switcher :refresh="true" :renderOnlyFocused="true">
       <Timeline
         :label="$t('user_card.statuses')"
         :disabled="!user.statuses_count"
-- 
cgit v1.2.3-70-g09d2


From 5a0bb29f02349aed4a7948b963ff0a78d3975237 Mon Sep 17 00:00:00 2001
From: dave <starpumadev@gmail.com>
Date: Sun, 3 Mar 2019 13:38:48 -0500
Subject: #417: reset tab from the outside

---
 src/components/tab_switcher/tab_switcher.js  | 9 +--------
 src/components/user_profile/user_profile.js  | 3 +++
 src/components/user_profile/user_profile.vue | 2 +-
 3 files changed, 5 insertions(+), 9 deletions(-)

(limited to 'src/components/user_profile/user_profile.vue')

diff --git a/src/components/tab_switcher/tab_switcher.js b/src/components/tab_switcher/tab_switcher.js
index 03da8249..423df258 100644
--- a/src/components/tab_switcher/tab_switcher.js
+++ b/src/components/tab_switcher/tab_switcher.js
@@ -4,19 +4,12 @@ import './tab_switcher.scss'
 
 export default Vue.component('tab-switcher', {
   name: 'TabSwitcher',
-  props: ['refresh', 'renderOnlyFocused'],
+  props: ['renderOnlyFocused'],
   data () {
     return {
       active: this.$slots.default.findIndex(_ => _.tag)
     }
   },
-  watch: {
-    $route () {
-      if (this.refresh) {
-        this.active = 0
-      }
-    }
-  },
   methods: {
     activateTab (index) {
       return () => {
diff --git a/src/components/user_profile/user_profile.js b/src/components/user_profile/user_profile.js
index 7708141c..cdf1cee9 100644
--- a/src/components/user_profile/user_profile.js
+++ b/src/components/user_profile/user_profile.js
@@ -141,6 +141,9 @@ const UserProfile = {
       }
       this.cleanUp()
       this.startUp()
+    },
+    $route () {
+      this.$refs.tabSwitcher.activateTab(0)()
     }
   },
   components: {
diff --git a/src/components/user_profile/user_profile.vue b/src/components/user_profile/user_profile.vue
index 54f1b97b..8090efa5 100644
--- a/src/components/user_profile/user_profile.vue
+++ b/src/components/user_profile/user_profile.vue
@@ -6,7 +6,7 @@
       :switcher="true"
       :selected="timeline.viewing"
     />
-    <tab-switcher :refresh="true" :renderOnlyFocused="true">
+    <tab-switcher :renderOnlyFocused="true" ref="tabSwitcher">
       <Timeline
         :label="$t('user_card.statuses')"
         :disabled="!user.statuses_count"
-- 
cgit v1.2.3-70-g09d2

