From 49b0f0a04a74039a1b82fbde731828e599123e93 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Sat, 9 Mar 2019 18:33:49 +0200
Subject: [PATCH 01/40] Fetching convos via MastoAPI. Had to change
 conversation component a bit for better support, since MastoAPI doesn't have
 coversation ids

---
 src/components/conversation/conversation.js | 23 +++++++-------
 src/services/api/api.service.js             | 35 ++++++++++++++-------
 2 files changed, 35 insertions(+), 23 deletions(-)

diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js
index 48b8aaaa..fd4303ca 100644
--- a/src/components/conversation/conversation.js
+++ b/src/components/conversation/conversation.js
@@ -25,7 +25,8 @@ const sortAndFilterConversation = (conversation) => {
 const conversation = {
   data () {
     return {
-      highlight: null
+      highlight: null,
+      relevantIds: []
     }
   },
   props: [
@@ -48,9 +49,11 @@ const conversation = {
         return []
       }
 
-      const conversationId = this.status.statusnet_conversation_id
-      const statuses = this.$store.state.statuses.allStatuses
-      const conversation = filter(statuses, { statusnet_conversation_id: conversationId })
+      const statusesObject = this.$store.state.statuses.allStatusesObject
+      const conversation = this.relevantIds.reduce((acc, id) => {
+        acc.push(statusesObject[id])
+        return acc
+      }, [])
       return sortAndFilterConversation(conversation)
     },
     replies () {
@@ -83,15 +86,13 @@ const conversation = {
   methods: {
     fetchConversation () {
       if (this.status) {
-        const conversationId = this.status.statusnet_conversation_id
+        const conversationId = this.status.id
         this.$store.state.api.backendInteractor.fetchConversation({id: conversationId})
-          .then((statuses) => this.$store.dispatch('addNewStatuses', { statuses }))
+          .then((statuses) => {
+            this.$store.dispatch('addNewStatuses', { statuses })
+            statuses.forEach(status => this.relevantIds.push(status.id))
+          })
           .then(() => this.setHighlight(this.statusId))
-      } else {
-        const id = this.$route.params.id
-        this.$store.state.api.backendInteractor.fetchStatus({id})
-          .then((status) => this.$store.dispatch('addNewStatuses', { statuses: [status] }))
-          .then(() => this.fetchConversation())
       }
     },
     getReplies (id) {
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index 2de87026..a273f32e 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -11,9 +11,7 @@ const RETWEET_URL = '/api/statuses/retweet'
 const UNRETWEET_URL = '/api/statuses/unretweet'
 const STATUS_UPDATE_URL = '/api/statuses/update.json'
 const STATUS_DELETE_URL = '/api/statuses/destroy'
-const STATUS_URL = '/api/statuses/show'
 const MEDIA_UPLOAD_URL = '/api/statusnet/media/upload'
-const CONVERSATION_URL = '/api/statusnet/conversation'
 const MENTIONS_URL = '/api/statuses/mentions.json'
 const DM_TIMELINE_URL = '/api/statuses/dm_timeline.json'
 const FOLLOWERS_URL = '/api/statuses/followers.json'
@@ -43,6 +41,8 @@ const DENY_USER_URL = '/api/pleroma/friendships/deny'
 const SUGGESTIONS_URL = '/api/v1/suggestions'
 
 const MASTODON_USER_FAVORITES_TIMELINE_URL = '/api/v1/favourites'
+const MASTODON_STATUS_URL = id => `/api/v1/statuses/${id}`
+const MASTODON_STATUS_CONTEXT_URL = id => `/api/v1/statuses/${id}/context`
 
 import { each, map } from 'lodash'
 import { parseStatus, parseUser, parseNotification } from '../entity_normalizer/entity_normalizer.service.js'
@@ -298,20 +298,31 @@ const fetchFollowRequests = ({credentials}) => {
 }
 
 const fetchConversation = ({id, credentials}) => {
-  let url = `${CONVERSATION_URL}/${id}.json?count=100`
-  return fetch(url, { headers: authHeaders(credentials) })
-    .then((data) => {
-      if (data.ok) {
-        return data
-      }
-      throw new Error('Error fetching timeline', data)
-    })
-    .then((data) => data.json())
+  let url = MASTODON_STATUS_URL(id)
+  let urlContext = MASTODON_STATUS_CONTEXT_URL(id)
+  return Promise.all([
+    fetch(url, { headers: authHeaders(credentials) })
+      .then((data) => {
+        if (data.ok) {
+          return data
+        }
+        throw new Error('Error fetching timeline', data)
+      })
+      .then((data) => data.json()),
+    fetch(urlContext, { headers: authHeaders(credentials) })
+      .then((data) => {
+        if (data.ok) {
+          return data
+        }
+        throw new Error('Error fetching timeline', data)
+      })
+      .then((data) => data.json())])
+    .then(([status, context]) => [...context.ancestors, status, ...context.descendants])
     .then((data) => data.map(parseStatus))
 }
 
 const fetchStatus = ({id, credentials}) => {
-  let url = `${STATUS_URL}/${id}.json`
+  let url = MASTODON_STATUS_URL(id)
   return fetch(url, { headers: authHeaders(credentials) })
     .then((data) => {
       if (data.ok) {

From 6e60873a3d9c709689eb02c05460b25a73ea0718 Mon Sep 17 00:00:00 2001
From: taehoon <th.dev91@gmail.com>
Date: Tue, 12 Mar 2019 14:18:20 -0400
Subject: [PATCH 02/40] Switch to mastoapi for user search

---
 src/services/new_api/user_search.js | 9 ++++++---
 1 file changed, 6 insertions(+), 3 deletions(-)

diff --git a/src/services/new_api/user_search.js b/src/services/new_api/user_search.js
index ce7da88e..869afa9c 100644
--- a/src/services/new_api/user_search.js
+++ b/src/services/new_api/user_search.js
@@ -1,13 +1,16 @@
 import utils from './utils.js'
+import { parseUser } from '../entity_normalizer/entity_normalizer.service.js'
 
 const search = ({query, store}) => {
   return utils.request({
     store,
-    url: '/api/pleroma/search_user',
+    url: '/api/v1/accounts/search',
     params: {
-      query
+      q: query
     }
-  }).then((data) => data.json())
+  })
+  .then((data) => data.json())
+  .then((data) => data.map(parseUser))
 }
 const UserSearch = {
   search

From 1c6e5d36886ac383ae91059e6bc93274d896c54e Mon Sep 17 00:00:00 2001
From: dave <starpumadev@gmail.com>
Date: Thu, 21 Mar 2019 13:06:37 -0400
Subject: [PATCH 03/40] #451 - long username on follow/follower tabs

---
 src/components/basic_user_card/basic_user_card.vue | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/src/components/basic_user_card/basic_user_card.vue b/src/components/basic_user_card/basic_user_card.vue
index 9b80c72b..37c34538 100644
--- a/src/components/basic_user_card/basic_user_card.vue
+++ b/src/components/basic_user_card/basic_user_card.vue
@@ -52,6 +52,14 @@
       width: 16px;
       vertical-align: middle;
     }
+
+    span {
+      display: inline-block;
+      max-width: 100%;
+      overflow: hidden;
+      white-space: nowrap;
+      text-overflow: ellipsis;
+    }
   }
 
   &-expanded-content {

From dfcdf4f32cf1779ecd85271dabc2919cdc51783a Mon Sep 17 00:00:00 2001
From: dave <starpumadev@gmail.com>
Date: Thu, 21 Mar 2019 13:19:10 -0400
Subject: [PATCH 04/40] #452 - unfollow button issue

---
 src/components/follow_card/follow_card.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/follow_card/follow_card.vue b/src/components/follow_card/follow_card.vue
index 9bd21cfd..72a1de62 100644
--- a/src/components/follow_card/follow_card.vue
+++ b/src/components/follow_card/follow_card.vue
@@ -24,7 +24,7 @@
           {{ $t('user_card.follow') }}
         </template>
       </button>
-      <button v-if="following" class="btn btn-default pressed" @click="unfollowUser" :disabled="inProgress">
+      <button v-if="following" class="btn btn-default btn-follow pressed" @click="unfollowUser" :disabled="inProgress">
         <template v-if="inProgress">
           {{ $t('user_card.follow_progress') }}
         </template>

From 8932c5370609892af240d31af267a32fcdd17e81 Mon Sep 17 00:00:00 2001
From: dave <starpumadev@gmail.com>
Date: Thu, 21 Mar 2019 14:58:28 -0400
Subject: [PATCH 05/40] #451 - update styling

---
 src/components/basic_user_card/basic_user_card.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/basic_user_card/basic_user_card.vue b/src/components/basic_user_card/basic_user_card.vue
index 37c34538..4a24e810 100644
--- a/src/components/basic_user_card/basic_user_card.vue
+++ b/src/components/basic_user_card/basic_user_card.vue
@@ -53,7 +53,7 @@
       vertical-align: middle;
     }
 
-    span {
+    > span {
       display: inline-block;
       max-width: 100%;
       overflow: hidden;

From 23bae6714658ec17240a6964b2fb3f91c065e73e Mon Sep 17 00:00:00 2001
From: dave <starpumadev@gmail.com>
Date: Thu, 21 Mar 2019 15:03:54 -0400
Subject: [PATCH 06/40] #452 - update button class name

---
 src/components/follow_card/follow_card.vue | 24 ++++++++++++----------
 1 file changed, 13 insertions(+), 11 deletions(-)

diff --git a/src/components/follow_card/follow_card.vue b/src/components/follow_card/follow_card.vue
index 72a1de62..9f314fd3 100644
--- a/src/components/follow_card/follow_card.vue
+++ b/src/components/follow_card/follow_card.vue
@@ -4,12 +4,12 @@
       <span class="faint" v-if="!noFollowsYou && user.follows_you">
         {{ isMe ? $t('user_card.its_you') : $t('user_card.follows_you') }}
       </span>
-      <div class="btn-follow" v-if="showFollow && !loggedIn">
+      <div class="follow-card-follow-button" v-if="showFollow && !loggedIn">
         <RemoteFollow :user="user" />
       </div>
       <button
         v-if="showFollow && loggedIn"
-        class="btn btn-default btn-follow"
+        class="btn btn-default follow-card-follow-button"
         @click="followUser"
         :disabled="inProgress"
         :title="requestSent ? $t('user_card.follow_again') : ''"
@@ -24,7 +24,7 @@
           {{ $t('user_card.follow') }}
         </template>
       </button>
-      <button v-if="following" class="btn btn-default btn-follow pressed" @click="unfollowUser" :disabled="inProgress">
+      <button v-if="following" class="btn btn-default follow-card-follow-button pressed" @click="unfollowUser" :disabled="inProgress">
         <template v-if="inProgress">
           {{ $t('user_card.follow_progress') }}
         </template>
@@ -39,15 +39,17 @@
 <script src="./follow_card.js"></script>
 
 <style lang="scss">
-.follow-card-content-container {
-  flex-shrink: 0;
-  display: flex;
-  flex-direction: row;
-  justify-content: space-between;
-  flex-wrap: wrap;
-  line-height: 1.5em;
+.follow-card {
+  &-content-container {
+    flex-shrink: 0;
+    display: flex;
+    flex-direction: row;
+    justify-content: space-between;
+    flex-wrap: wrap;
+    line-height: 1.5em;
+  }
 
-  .btn-follow {
+  &-follow-button {
     margin-top: 0.5em;
     margin-left: auto;
     width: 10em;

From 3255950b0e9a16f2a477d606b91d90bed8a6cef7 Mon Sep 17 00:00:00 2001
From: taehoon <th.dev91@gmail.com>
Date: Sun, 24 Feb 2019 03:02:04 -0500
Subject: [PATCH 07/40] Add mute/unmute featrue and mutes management tab

---
 src/components/mute_card/mute_card.js         |  2 +-
 src/components/user_card/user_card.js         | 20 ++++----
 src/components/user_card/user_card.vue        |  4 +-
 .../user_settings/user_settings.vue           |  6 +++
 src/modules/users.js                          | 32 ++++++++-----
 src/services/api/api.service.js               | 46 +++++++++++++++++--
 .../backend_interactor_service.js             |  6 ++-
 src/utils/url.js                              |  9 ++++
 8 files changed, 94 insertions(+), 31 deletions(-)
 create mode 100644 src/utils/url.js

diff --git a/src/components/mute_card/mute_card.js b/src/components/mute_card/mute_card.js
index 65c9cfb5..5ef17b60 100644
--- a/src/components/mute_card/mute_card.js
+++ b/src/components/mute_card/mute_card.js
@@ -12,7 +12,7 @@ const MuteCard = {
       return this.$store.getters.findUser(this.userId)
     },
     muted () {
-      return this.user.muted
+      return this.user.mastodonMuted
     }
   },
   components: {
diff --git a/src/components/user_card/user_card.js b/src/components/user_card/user_card.js
index b07da675..61b784fe 100644
--- a/src/components/user_card/user_card.js
+++ b/src/components/user_card/user_card.js
@@ -121,21 +121,19 @@ export default {
       })
     },
     blockUser () {
-      const store = this.$store
-      store.state.api.backendInteractor.blockUser(this.user.id)
-        .then((blockedUser) => {
-          store.commit('addNewUsers', [blockedUser])
-          store.commit('removeStatus', { timeline: 'friends', userId: this.user.id })
-          store.commit('removeStatus', { timeline: 'public', userId: this.user.id })
-          store.commit('removeStatus', { timeline: 'publicAndExternal', userId: this.user.id })
-        })
+      this.$store.dispatch('blockUser', this.user.id)
     },
     unblockUser () {
-      const store = this.$store
-      store.state.api.backendInteractor.unblockUser(this.user.id)
-        .then((unblockedUser) => store.commit('addNewUsers', [unblockedUser]))
+      this.$store.dispatch('unblockUser', this.user.id)
+    },
+    muteUser () {   // Mastodon Mute
+      this.$store.dispatch('muteUser', this.user.id)
+    },
+    unmuteUser () { // Mastodon Unmute
+      this.$store.dispatch('unmuteUser', this.user.id)
     },
     toggleMute () {
+      // TODO: Pleroma mute/unmute, Need to migrate to the Mastodon API
       const store = this.$store
       store.commit('setMuted', {user: this.user, muted: !this.user.muted})
       store.state.api.backendInteractor.setUserMute(this.user)
diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue
index f4114e6e..3259d1c5 100644
--- a/src/components/user_card/user_card.vue
+++ b/src/components/user_card/user_card.vue
@@ -74,12 +74,12 @@
         </div>
         <div class='mute' v-if='isOtherUser && loggedIn'>
           <span v-if='user.muted'>
-            <button @click="toggleMute" class="pressed">
+            <button @click="unmuteUser" class="pressed">
               {{ $t('user_card.muted') }}
             </button>
           </span>
           <span v-if='!user.muted'>
-            <button @click="toggleMute">
+            <button @click="muteUser">
               {{ $t('user_card.mute') }}
             </button>
           </span>
diff --git a/src/components/user_settings/user_settings.vue b/src/components/user_settings/user_settings.vue
index a1123638..c9e68808 100644
--- a/src/components/user_settings/user_settings.vue
+++ b/src/components/user_settings/user_settings.vue
@@ -192,6 +192,12 @@
             <template slot="empty">{{$t('settings.no_blocks')}}</template>
           </block-list>
         </div>
+
+        <div :label="$t('settings.mutes_tab')">
+          <mute-list :refresh="true">
+            <template slot="empty">{{$t('settings.no_mutes')}}</template>
+          </mute-list>
+        </div>
       </tab-switcher>
     </div>
   </div>
diff --git a/src/modules/users.js b/src/modules/users.js
index 1fe12fc8..9c89f34a 100644
--- a/src/modules/users.js
+++ b/src/modules/users.js
@@ -177,9 +177,14 @@ const users = {
           return blocks
         })
     },
-    blockUser (store, id) {
-      return store.rootState.api.backendInteractor.blockUser(id)
-        .then((user) => store.commit('addNewUsers', [user]))
+    blockUser (store, userId) {
+      return store.rootState.api.backendInteractor.blockUser(userId)
+        .then((blockedUser) => {
+          store.commit('addNewUsers', [blockedUser])
+          store.commit('removeStatus', { timeline: 'friends', userId })
+          store.commit('removeStatus', { timeline: 'public', userId })
+          store.commit('removeStatus', { timeline: 'publicAndExternal', userId })
+        })
     },
     unblockUser (store, id) {
       return store.rootState.api.backendInteractor.unblockUser(id)
@@ -188,18 +193,26 @@ const users = {
     fetchMutes (store) {
       return store.rootState.api.backendInteractor.fetchMutes()
         .then((mutedUsers) => {
-          each(mutedUsers, (user) => { user.muted = true })
+          each(mutedUsers, (user) => { user.mastodonMuted = true })
           store.commit('addNewUsers', mutedUsers)
           store.commit('saveMutes', map(mutedUsers, 'id'))
         })
     },
     muteUser (store, id) {
-      return store.state.api.backendInteractor.setUserMute({ id, muted: true })
-        .then((user) => store.commit('addNewUsers', [user]))
+      return store.rootState.api.backendInteractor.muteUser(id)
+        .then(() => {
+          const user = store.rootState.users.usersObject[id]
+          set(user, 'mastodonMuted', true)
+          store.commit('addNewUsers', [user])
+        })
     },
     unmuteUser (store, id) {
-      return store.state.api.backendInteractor.setUserMute({ id, muted: false })
-        .then((user) => store.commit('addNewUsers', [user]))
+      return store.rootState.api.backendInteractor.unmuteUser(id)
+        .then(() => {
+          const user = store.rootState.users.usersObject[id]
+          set(user, 'mastodonMuted', false)
+          store.commit('addNewUsers', [user])
+        })
     },
     addFriends ({ rootState, commit }, fetchBy) {
       return new Promise((resolve, reject) => {
@@ -350,9 +363,6 @@ const users = {
               // Start getting fresh posts.
               store.dispatch('startFetching', { timeline: 'friends' })
 
-              // Get user mutes
-              store.dispatch('fetchMutes')
-
               // Fetch our friends
               store.rootState.api.backendInteractor.fetchFriends({ id: user.id })
                 .then((friends) => commit('addNewUsers', friends))
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index 176f1c18..92abf94b 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -1,3 +1,5 @@
+import { generateUrl } from '../../utils/url'
+
 /* eslint-env browser */
 const LOGIN_URL = '/api/account/verify_credentials.json'
 const FRIENDS_TIMELINE_URL = '/api/statuses/friends_timeline.json'
@@ -19,6 +21,9 @@ const DM_TIMELINE_URL = '/api/statuses/dm_timeline.json'
 const FOLLOWERS_URL = '/api/statuses/followers.json'
 const FRIENDS_URL = '/api/statuses/friends.json'
 const BLOCKS_URL = '/api/statuses/blocks.json'
+const MUTES_URL = '/api/v1/mutes.json'
+const MUTING_URL = '/api/v1/accounts/:id/mute'
+const UNMUTING_URL = '/api/v1/accounts/:id/unmute'
 const FOLLOWING_URL = '/api/friendships/create.json'
 const UNFOLLOWING_URL = '/api/friendships/destroy.json'
 const QVITTER_USER_PREF_URL = '/api/qvitter/set_profile_pref.json'
@@ -538,14 +543,43 @@ const changePassword = ({credentials, password, newPassword, newPasswordConfirma
 }
 
 const fetchMutes = ({credentials}) => {
-  const url = '/api/qvitter/mutes.json'
-
-  return fetch(url, {
+  return fetch(MUTES_URL, {
     headers: authHeaders(credentials)
-  }).then((data) => data.json())
+  }).then((data) => {
+    if (data.ok) {
+      return data.json()
+    }
+    throw new Error('Error fetching mutes', data)
+  })
 }
 
-const fetchBlocks = ({page, credentials}) => {
+const muteUser = ({id, credentials}) => {
+  const url = generateUrl(MUTING_URL, { id })
+  return fetch(url, {
+    headers: authHeaders(credentials),
+    method: 'POST'
+  }).then((data) => {
+    if (data.ok) {
+      return data.json()
+    }
+    throw new Error('Error muting', data)
+  })
+}
+
+const unmuteUser = ({id, credentials}) => {
+  const url = generateUrl(UNMUTING_URL, { id })
+  return fetch(url, {
+    headers: authHeaders(credentials),
+    method: 'POST'
+  }).then((data) => {
+    if (data.ok) {
+      return data.json()
+    }
+    throw new Error('Error unmuting', data)
+  })
+}
+
+const fetchBlocks = ({credentials}) => {
   return fetch(BLOCKS_URL, {
     headers: authHeaders(credentials)
   }).then((data) => {
@@ -620,6 +654,8 @@ const apiService = {
   fetchAllFollowing,
   setUserMute,
   fetchMutes,
+  muteUser,
+  unmuteUser,
   fetchBlocks,
   fetchOAuthTokens,
   revokeOAuthToken,
diff --git a/src/services/backend_interactor_service/backend_interactor_service.js b/src/services/backend_interactor_service/backend_interactor_service.js
index cbd0b733..674fb4a4 100644
--- a/src/services/backend_interactor_service/backend_interactor_service.js
+++ b/src/services/backend_interactor_service/backend_interactor_service.js
@@ -67,7 +67,9 @@ const backendInteractorService = (credentials) => {
   }
 
   const fetchMutes = () => apiService.fetchMutes({credentials})
-  const fetchBlocks = (params) => apiService.fetchBlocks({credentials, ...params})
+  const muteUser = (id) => apiService.muteUser({credentials, id})
+  const unmuteUser = (id) => apiService.unmuteUser({credentials, id})
+  const fetchBlocks = () => apiService.fetchBlocks({credentials})
   const fetchFollowRequests = () => apiService.fetchFollowRequests({credentials})
   const fetchOAuthTokens = () => apiService.fetchOAuthTokens({credentials})
   const revokeOAuthToken = (id) => apiService.revokeOAuthToken({id, credentials})
@@ -102,6 +104,8 @@ const backendInteractorService = (credentials) => {
     startFetching,
     setUserMute,
     fetchMutes,
+    muteUser,
+    unmuteUser,
     fetchBlocks,
     fetchOAuthTokens,
     revokeOAuthToken,
diff --git a/src/utils/url.js b/src/utils/url.js
new file mode 100644
index 00000000..79ea7394
--- /dev/null
+++ b/src/utils/url.js
@@ -0,0 +1,9 @@
+// Generate url based on template
+// Example: /api/v1/accounts/:id/mute -> /api/v1/accounts/123/mute
+export const generateUrl = (template, params = {}) => {
+  let url = template
+  Object.entries(params).forEach(([key, value]) => {
+    url = url.replace(':' + key, value)
+  })
+  return url
+}

From f04cbc887595e842ab8b9dad6545f6e409e52ea1 Mon Sep 17 00:00:00 2001
From: taehoon <th.dev91@gmail.com>
Date: Sun, 24 Feb 2019 03:20:11 -0500
Subject: [PATCH 08/40] Add mute/unmute mutations

---
 src/modules/users.js | 20 ++++++++++----------
 1 file changed, 10 insertions(+), 10 deletions(-)

diff --git a/src/modules/users.js b/src/modules/users.js
index 9c89f34a..f53245a2 100644
--- a/src/modules/users.js
+++ b/src/modules/users.js
@@ -108,6 +108,14 @@ export const mutations = {
   saveMutes (state, muteIds) {
     state.currentUser.muteIds = muteIds
   },
+  muteUser (state, id) {
+    const user = state.usersObject[id]
+    set(user, 'mastodonMuted', true)
+  },
+  unmuteUser (state, id) {
+    const user = state.usersObject[id]
+    set(user, 'mastodonMuted', false)
+  },
   setUserForStatus (state, status) {
     status.user = state.usersObject[status.user.id]
   },
@@ -200,19 +208,11 @@ const users = {
     },
     muteUser (store, id) {
       return store.rootState.api.backendInteractor.muteUser(id)
-        .then(() => {
-          const user = store.rootState.users.usersObject[id]
-          set(user, 'mastodonMuted', true)
-          store.commit('addNewUsers', [user])
-        })
+        .then(() => store.commit('muteUser', id))
     },
     unmuteUser (store, id) {
       return store.rootState.api.backendInteractor.unmuteUser(id)
-        .then(() => {
-          const user = store.rootState.users.usersObject[id]
-          set(user, 'mastodonMuted', false)
-          store.commit('addNewUsers', [user])
-        })
+        .then(() => store.commit('unmuteUser', id))
     },
     addFriends ({ rootState, commit }, fetchBy) {
       return new Promise((resolve, reject) => {

From 859ed4f34feffd4a975e6d865f82bc57224025f9 Mon Sep 17 00:00:00 2001
From: taehoon <th.dev91@gmail.com>
Date: Sun, 24 Feb 2019 03:21:21 -0500
Subject: [PATCH 09/40] Fetct full data of muted users after fetchMutes api
 call

---
 src/modules/users.js | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/src/modules/users.js b/src/modules/users.js
index f53245a2..af40be3d 100644
--- a/src/modules/users.js
+++ b/src/modules/users.js
@@ -200,6 +200,11 @@ const users = {
     },
     fetchMutes (store) {
       return store.rootState.api.backendInteractor.fetchMutes()
+        .then((mutes) => {
+          // fetchMutes api doesn't return full user data, let's fetch full user data using separate api calls
+          const promises = mutes.map(({ id }) => store.rootState.api.backendInteractor.fetchUser({ id }))
+          return Promise.all(promises)
+        })
         .then((mutedUsers) => {
           each(mutedUsers, (user) => { user.mastodonMuted = true })
           store.commit('addNewUsers', mutedUsers)

From a6ce191cbcc34f309e9c4d4e3f7e3c11ed020821 Mon Sep 17 00:00:00 2001
From: taehoon <th.dev91@gmail.com>
Date: Thu, 28 Feb 2019 22:59:26 -0500
Subject: [PATCH 10/40] Update MuteCard ui

---
 src/components/mute_card/mute_card.vue | 16 +++++++++++++---
 1 file changed, 13 insertions(+), 3 deletions(-)

diff --git a/src/components/mute_card/mute_card.vue b/src/components/mute_card/mute_card.vue
index e1bfe20b..a4edff03 100644
--- a/src/components/mute_card/mute_card.vue
+++ b/src/components/mute_card/mute_card.vue
@@ -1,6 +1,6 @@
 <template>
   <basic-user-card :user="user">
-    <template slot="secondary-area">
+    <div class="mute-card-content-container">
       <button class="btn btn-default" @click="unmuteUser" :disabled="progress" v-if="muted">
         <template v-if="progress">
           {{ $t('user_card.unmute_progress') }}
@@ -17,8 +17,18 @@
           {{ $t('user_card.mute') }}
         </template>
       </button>
-    </template>
+    </div>
   </basic-user-card>
 </template>
 
-<script src="./mute_card.js"></script>
\ No newline at end of file
+<script src="./mute_card.js"></script>
+
+<style lang="scss">
+.mute-card-content-container {
+  margin-top: 0.5em;
+  text-align: right;
+  button {
+    width: 10em;
+  }
+}
+</style>

From 302310a653083bc82226cf0743d52fc02c277a8a Mon Sep 17 00:00:00 2001
From: taehoon <th.dev91@gmail.com>
Date: Fri, 1 Mar 2019 11:37:34 -0500
Subject: [PATCH 11/40] Remove old muting logic

---
 src/components/mute_card/mute_card.js          |  2 +-
 src/components/user_card/user_card.js          | 10 ++--------
 src/modules/users.js                           | 12 +++++++++---
 src/services/api/api.service.js                | 18 ------------------
 .../backend_interactor_service.js              |  5 -----
 5 files changed, 12 insertions(+), 35 deletions(-)

diff --git a/src/components/mute_card/mute_card.js b/src/components/mute_card/mute_card.js
index 5ef17b60..65c9cfb5 100644
--- a/src/components/mute_card/mute_card.js
+++ b/src/components/mute_card/mute_card.js
@@ -12,7 +12,7 @@ const MuteCard = {
       return this.$store.getters.findUser(this.userId)
     },
     muted () {
-      return this.user.mastodonMuted
+      return this.user.muted
     }
   },
   components: {
diff --git a/src/components/user_card/user_card.js b/src/components/user_card/user_card.js
index 61b784fe..197c61d5 100644
--- a/src/components/user_card/user_card.js
+++ b/src/components/user_card/user_card.js
@@ -126,18 +126,12 @@ export default {
     unblockUser () {
       this.$store.dispatch('unblockUser', this.user.id)
     },
-    muteUser () {   // Mastodon Mute
+    muteUser () {
       this.$store.dispatch('muteUser', this.user.id)
     },
-    unmuteUser () { // Mastodon Unmute
+    unmuteUser () {
       this.$store.dispatch('unmuteUser', this.user.id)
     },
-    toggleMute () {
-      // TODO: Pleroma mute/unmute, Need to migrate to the Mastodon API
-      const store = this.$store
-      store.commit('setMuted', {user: this.user, muted: !this.user.muted})
-      store.state.api.backendInteractor.setUserMute(this.user)
-    },
     setProfileView (v) {
       if (this.switcher) {
         const store = this.$store
diff --git a/src/modules/users.js b/src/modules/users.js
index af40be3d..5e53aafb 100644
--- a/src/modules/users.js
+++ b/src/modules/users.js
@@ -110,11 +110,11 @@ export const mutations = {
   },
   muteUser (state, id) {
     const user = state.usersObject[id]
-    set(user, 'mastodonMuted', true)
+    set(user, 'muted', true)
   },
   unmuteUser (state, id) {
     const user = state.usersObject[id]
-    set(user, 'mastodonMuted', false)
+    set(user, 'muted', false)
   },
   setUserForStatus (state, status) {
     status.user = state.usersObject[status.user.id]
@@ -206,9 +206,10 @@ const users = {
           return Promise.all(promises)
         })
         .then((mutedUsers) => {
-          each(mutedUsers, (user) => { user.mastodonMuted = true })
+          each(mutedUsers, (user) => { user.muted = true })
           store.commit('addNewUsers', mutedUsers)
           store.commit('saveMutes', map(mutedUsers, 'id'))
+          // TODO: Unset muted property of the rest users
         })
     },
     muteUser (store, id) {
@@ -368,6 +369,11 @@ const users = {
               // Start getting fresh posts.
               store.dispatch('startFetching', { timeline: 'friends' })
 
+              // Fetch mutes
+              // TODO: We should not show timeline until fetchMutes is resolved
+              // However, we can get rid of this logic totally if we can know user muted state from user object
+              store.dispatch('fetchMutes')
+
               // Fetch our friends
               store.rootState.api.backendInteractor.fetchFriends({ id: user.id })
                 .then((friends) => commit('addNewUsers', friends))
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index 92abf94b..7da2758a 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -26,7 +26,6 @@ const MUTING_URL = '/api/v1/accounts/:id/mute'
 const UNMUTING_URL = '/api/v1/accounts/:id/unmute'
 const FOLLOWING_URL = '/api/friendships/create.json'
 const UNFOLLOWING_URL = '/api/friendships/destroy.json'
-const QVITTER_USER_PREF_URL = '/api/qvitter/set_profile_pref.json'
 const REGISTRATION_URL = '/api/account/register.json'
 const AVATAR_UPDATE_URL = '/api/qvitter/update_avatar.json'
 const BG_UPDATE_URL = '/api/qvitter/update_background_image.json'
@@ -343,22 +342,6 @@ const fetchStatus = ({id, credentials}) => {
     .then((data) => parseStatus(data))
 }
 
-const setUserMute = ({id, credentials, muted = true}) => {
-  const form = new FormData()
-
-  const muteInteger = muted ? 1 : 0
-
-  form.append('namespace', 'qvitter')
-  form.append('data', muteInteger)
-  form.append('topic', `mute:${id}`)
-
-  return fetch(QVITTER_USER_PREF_URL, {
-    method: 'POST',
-    headers: authHeaders(credentials),
-    body: form
-  })
-}
-
 const fetchTimeline = ({timeline, credentials, since = false, until = false, userId = false, tag = false}) => {
   const timelineUrls = {
     public: PUBLIC_TIMELINE_URL,
@@ -652,7 +635,6 @@ const apiService = {
   deleteStatus,
   uploadMedia,
   fetchAllFollowing,
-  setUserMute,
   fetchMutes,
   muteUser,
   unmuteUser,
diff --git a/src/services/backend_interactor_service/backend_interactor_service.js b/src/services/backend_interactor_service/backend_interactor_service.js
index 674fb4a4..0f0bcddc 100644
--- a/src/services/backend_interactor_service/backend_interactor_service.js
+++ b/src/services/backend_interactor_service/backend_interactor_service.js
@@ -62,10 +62,6 @@ const backendInteractorService = (credentials) => {
     return timelineFetcherService.startFetching({timeline, store, credentials, userId, tag})
   }
 
-  const setUserMute = ({id, muted = true}) => {
-    return apiService.setUserMute({id, muted, credentials})
-  }
-
   const fetchMutes = () => apiService.fetchMutes({credentials})
   const muteUser = (id) => apiService.muteUser({credentials, id})
   const unmuteUser = (id) => apiService.unmuteUser({credentials, id})
@@ -102,7 +98,6 @@ const backendInteractorService = (credentials) => {
     fetchAllFollowing,
     verifyCredentials: apiService.verifyCredentials,
     startFetching,
-    setUserMute,
     fetchMutes,
     muteUser,
     unmuteUser,

From 9b690209d056d82fa855b682ffeb74378128460e Mon Sep 17 00:00:00 2001
From: taehoon <th.dev91@gmail.com>
Date: Fri, 1 Mar 2019 13:30:01 -0500
Subject: [PATCH 12/40] Reset old mutes state after fetching new mutes data

---
 src/modules/users.js | 20 ++++++++++++--------
 1 file changed, 12 insertions(+), 8 deletions(-)

diff --git a/src/modules/users.js b/src/modules/users.js
index 5e53aafb..668ebabd 100644
--- a/src/modules/users.js
+++ b/src/modules/users.js
@@ -102,10 +102,15 @@ export const mutations = {
       }
     })
   },
-  saveBlocks (state, blockIds) {
+  saveBlockIds (state, blockIds) {
     state.currentUser.blockIds = blockIds
   },
-  saveMutes (state, muteIds) {
+  updateMutes (state, mutedUsers) {
+    each(state.users, (user) => { user.muted = false })
+    const newUsers = map(mutedUsers, (user) => ({ ...user, muted: true }))
+    each(newUsers, (user) => mergeOrAdd(state.users, state.usersObject, user))
+  },
+  saveMuteIds (state, muteIds) {
     state.currentUser.muteIds = muteIds
   },
   muteUser (state, id) {
@@ -180,7 +185,7 @@ const users = {
     fetchBlocks (store) {
       return store.rootState.api.backendInteractor.fetchBlocks()
         .then((blocks) => {
-          store.commit('saveBlocks', map(blocks, 'id'))
+          store.commit('saveBlockIds', map(blocks, 'id'))
           store.commit('addNewUsers', blocks)
           return blocks
         })
@@ -205,11 +210,10 @@ const users = {
           const promises = mutes.map(({ id }) => store.rootState.api.backendInteractor.fetchUser({ id }))
           return Promise.all(promises)
         })
-        .then((mutedUsers) => {
-          each(mutedUsers, (user) => { user.muted = true })
-          store.commit('addNewUsers', mutedUsers)
-          store.commit('saveMutes', map(mutedUsers, 'id'))
-          // TODO: Unset muted property of the rest users
+        .then((mutes) => {
+          store.commit('updateMutes', mutes)
+          store.commit('saveMuteIds', map(mutes, 'id'))
+          return mutes
         })
     },
     muteUser (store, id) {

From d65422a6a59e0126201f71d0ca42343c075da54e Mon Sep 17 00:00:00 2001
From: taehoon <th.dev91@gmail.com>
Date: Fri, 1 Mar 2019 22:47:07 -0500
Subject: [PATCH 13/40] Improve fetch error handling using a util

---
 src/services/api/api.service.js | 54 +++++++++++----------------------
 1 file changed, 18 insertions(+), 36 deletions(-)

diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index 7da2758a..b7fcf510 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -64,6 +64,19 @@ let fetch = (url, options) => {
   return oldfetch(fullUrl, options)
 }
 
+const promisedRequest = (url, options) => {
+  return fetch(url, options)
+    .then((response) => {
+      return new Promise((resolve, reject) => response.json()
+        .then((json) => {
+          if (!response.ok) {
+            return reject(new StatusCodeError(response.status, json, { url, options }, response))
+          }
+          return resolve(json)
+        }))
+    })
+}
+
 // Params
 // cropH
 // cropW
@@ -249,16 +262,7 @@ const denyUser = ({id, credentials}) => {
 
 const fetchUser = ({id, credentials}) => {
   let url = `${MASTODON_USER_URL}/${id}`
-  return fetch(url, { headers: authHeaders(credentials) })
-    .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)
-        }))
-    })
+  return promisedRequest(url, { headers: authHeaders(credentials) })
     .then((data) => parseUser(data))
 }
 
@@ -526,50 +530,28 @@ const changePassword = ({credentials, password, newPassword, newPasswordConfirma
 }
 
 const fetchMutes = ({credentials}) => {
-  return fetch(MUTES_URL, {
-    headers: authHeaders(credentials)
-  }).then((data) => {
-    if (data.ok) {
-      return data.json()
-    }
-    throw new Error('Error fetching mutes', data)
-  })
+  return promisedRequest(MUTES_URL, { headers: authHeaders(credentials) })
 }
 
 const muteUser = ({id, credentials}) => {
   const url = generateUrl(MUTING_URL, { id })
-  return fetch(url, {
+  return promisedRequest(url, {
     headers: authHeaders(credentials),
     method: 'POST'
-  }).then((data) => {
-    if (data.ok) {
-      return data.json()
-    }
-    throw new Error('Error muting', data)
   })
 }
 
 const unmuteUser = ({id, credentials}) => {
   const url = generateUrl(UNMUTING_URL, { id })
-  return fetch(url, {
+  return promisedRequest(url, {
     headers: authHeaders(credentials),
     method: 'POST'
-  }).then((data) => {
-    if (data.ok) {
-      return data.json()
-    }
-    throw new Error('Error unmuting', data)
   })
 }
 
 const fetchBlocks = ({credentials}) => {
-  return fetch(BLOCKS_URL, {
+  return promisedRequest(BLOCKS_URL, {
     headers: authHeaders(credentials)
-  }).then((data) => {
-    if (data.ok) {
-      return data.json()
-    }
-    throw new Error('Error fetching blocks', data)
   })
 }
 

From 333afd213892087f5047fabdc2cc21b70ddcf76d Mon Sep 17 00:00:00 2001
From: taehoon <th.dev91@gmail.com>
Date: Fri, 1 Mar 2019 22:50:06 -0500
Subject: [PATCH 14/40] Minor code refactoring

---
 src/services/api/api.service.js | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index b7fcf510..14bc919f 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -1,5 +1,3 @@
-import { generateUrl } from '../../utils/url'
-
 /* eslint-env browser */
 const LOGIN_URL = '/api/account/verify_credentials.json'
 const FRIENDS_TIMELINE_URL = '/api/statuses/friends_timeline.json'
@@ -53,6 +51,7 @@ import { each, map } from 'lodash'
 import { parseStatus, parseUser, parseNotification } from '../entity_normalizer/entity_normalizer.service.js'
 import 'whatwg-fetch'
 import { StatusCodeError } from '../errors/errors'
+import { generateUrl } from '../../utils/url'
 
 const oldfetch = window.fetch
 

From 300259fd976bb4496c5e1cca0df9bd47a1947259 Mon Sep 17 00:00:00 2001
From: taehoon <th.dev91@gmail.com>
Date: Sat, 2 Mar 2019 06:47:05 -0500
Subject: [PATCH 15/40] Add todo comment

---
 src/modules/users.js | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/modules/users.js b/src/modules/users.js
index 668ebabd..79046371 100644
--- a/src/modules/users.js
+++ b/src/modules/users.js
@@ -107,6 +107,7 @@ export const mutations = {
   },
   updateMutes (state, mutedUsers) {
     each(state.users, (user) => { user.muted = false })
+    // TODO: Remove this line once we get muted property of user object from the api
     const newUsers = map(mutedUsers, (user) => ({ ...user, muted: true }))
     each(newUsers, (user) => mergeOrAdd(state.users, state.usersObject, user))
   },

From 9857002bf5dc902302644e981712b611124a5845 Mon Sep 17 00:00:00 2001
From: taehoon <th.dev91@gmail.com>
Date: Sat, 2 Mar 2019 08:07:14 -0500
Subject: [PATCH 16/40] Add hideMutedPosts setting and wire up to
 post-returning endpoints

---
 src/boot/after_store.js                                   | 1 +
 src/components/settings/settings.js                       | 8 ++++++++
 src/components/settings/settings.vue                      | 4 ++++
 src/i18n/en.json                                          | 1 +
 src/modules/config.js                                     | 1 +
 src/modules/instance.js                                   | 1 +
 src/services/api/api.service.js                           | 3 ++-
 src/services/timeline_fetcher/timeline_fetcher.service.js | 4 ++++
 8 files changed, 22 insertions(+), 1 deletion(-)

diff --git a/src/boot/after_store.js b/src/boot/after_store.js
index a5f8c978..f5e84cbc 100644
--- a/src/boot/after_store.js
+++ b/src/boot/after_store.js
@@ -97,6 +97,7 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
   copyInstanceOption('showInstanceSpecificPanel')
   copyInstanceOption('scopeOptionsEnabled')
   copyInstanceOption('formattingOptionsEnabled')
+  copyInstanceOption('hideMutedPosts')
   copyInstanceOption('collapseMessageWithSubject')
   copyInstanceOption('loginMethod')
   copyInstanceOption('scopeCopy')
diff --git a/src/components/settings/settings.js b/src/components/settings/settings.js
index b77c5197..1d5f75ed 100644
--- a/src/components/settings/settings.js
+++ b/src/components/settings/settings.js
@@ -47,6 +47,11 @@ const settings = {
       pauseOnUnfocusedLocal: user.pauseOnUnfocused,
       hoverPreviewLocal: user.hoverPreview,
 
+      hideMutedPostsLocal: typeof user.hideMutedPosts === 'undefined'
+        ? instance.hideMutedPosts
+        : user.hideMutedPosts,
+      hideMutedPostsDefault: this.$t('settings.values.' + instance.hideMutedPosts),
+
       collapseMessageWithSubjectLocal: typeof user.collapseMessageWithSubject === 'undefined'
         ? instance.collapseMessageWithSubject
         : user.collapseMessageWithSubject,
@@ -177,6 +182,9 @@ const settings = {
       value = filter(value.split('\n'), (word) => trim(word).length > 0)
       this.$store.dispatch('setOption', { name: 'muteWords', value })
     },
+    hideMutedPostsLocal (value) {
+      this.$store.dispatch('setOption', { name: 'hideMutedPosts', value })
+    },
     collapseMessageWithSubjectLocal (value) {
       this.$store.dispatch('setOption', { name: 'collapseMessageWithSubject', value })
     },
diff --git a/src/components/settings/settings.vue b/src/components/settings/settings.vue
index 17f1f1a1..33dad549 100644
--- a/src/components/settings/settings.vue
+++ b/src/components/settings/settings.vue
@@ -36,6 +36,10 @@
         <div class="setting-item">
           <h2>{{$t('nav.timeline')}}</h2>
           <ul class="setting-list">
+            <li>
+              <input type="checkbox" id="hideMutedPosts" v-model="hideMutedPostsLocal">
+              <label for="hideMutedPosts">{{$t('settings.hide_muted_posts')}} {{$t('settings.instance_default', { value: hideMutedPostsDefault })}}</label>
+            </li>
             <li>
               <input type="checkbox" id="collapseMessageWithSubject" v-model="collapseMessageWithSubjectLocal">
               <label for="collapseMessageWithSubject">
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 68503f99..ecf9628f 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -153,6 +153,7 @@
     "general": "General",
     "hide_attachments_in_convo": "Hide attachments in conversations",
     "hide_attachments_in_tl": "Hide attachments in timeline",
+    "hide_muted_posts": "Hide posts of muted users",
     "max_thumbnails": "Maximum amount of thumbnails per post",
     "hide_isp": "Hide instance-specific panel",
     "preload_images": "Preload images",
diff --git a/src/modules/config.js b/src/modules/config.js
index 1c30c203..c5491c01 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -5,6 +5,7 @@ const browserLocale = (window.navigator.language || 'en').split('-')[0]
 
 const defaultState = {
   colors: {},
+  hideMutedPosts: undefined, // instance default
   collapseMessageWithSubject: undefined, // instance default
   hideAttachments: false,
   hideAttachmentsInConv: false,
diff --git a/src/modules/instance.js b/src/modules/instance.js
index 155aa2eb..f778ac4d 100644
--- a/src/modules/instance.js
+++ b/src/modules/instance.js
@@ -18,6 +18,7 @@ const defaultState = {
   scopeOptionsEnabled: true,
   formattingOptionsEnabled: false,
   alwaysShowSubjectInput: true,
+  hideMutedPosts: false,
   collapseMessageWithSubject: false,
   hidePostStats: false,
   hideUserStats: false,
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index 14bc919f..73ea15aa 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -345,7 +345,7 @@ const fetchStatus = ({id, credentials}) => {
     .then((data) => parseStatus(data))
 }
 
-const fetchTimeline = ({timeline, credentials, since = false, until = false, userId = false, tag = false}) => {
+const fetchTimeline = ({timeline, credentials, since = false, until = false, userId = false, tag = false, withMuted = false}) => {
   const timelineUrls = {
     public: PUBLIC_TIMELINE_URL,
     friends: FRIENDS_TIMELINE_URL,
@@ -381,6 +381,7 @@ const fetchTimeline = ({timeline, credentials, since = false, until = false, use
   }
 
   params.push(['count', 20])
+  params.push(['with_muted', withMuted])
 
   const queryString = map(params, (param) => `${param[0]}=${param[1]}`).join('&')
   url += `?${queryString}`
diff --git a/src/services/timeline_fetcher/timeline_fetcher.service.js b/src/services/timeline_fetcher/timeline_fetcher.service.js
index 6f99616f..8e954cdf 100644
--- a/src/services/timeline_fetcher/timeline_fetcher.service.js
+++ b/src/services/timeline_fetcher/timeline_fetcher.service.js
@@ -19,6 +19,9 @@ const fetchAndUpdate = ({store, credentials, timeline = 'friends', older = false
   const args = { timeline, credentials }
   const rootState = store.rootState || store.state
   const timelineData = rootState.statuses.timelines[camelCase(timeline)]
+  const hideMutedPosts = typeof rootState.config.hideMutedPosts === 'undefined'
+    ? rootState.instance.hideMutedPosts
+    : rootState.config.hideMutedPosts
 
   if (older) {
     args['until'] = until || timelineData.minId
@@ -28,6 +31,7 @@ const fetchAndUpdate = ({store, credentials, timeline = 'friends', older = false
 
   args['userId'] = userId
   args['tag'] = tag
+  args['withMuted'] = !hideMutedPosts
 
   const numStatusesBeforeFetch = timelineData.statuses.length
 

From d7919109ec30c5a75690d6c5b26f1ecebe74d7bf Mon Sep 17 00:00:00 2001
From: taehoon <th.dev91@gmail.com>
Date: Sat, 2 Mar 2019 08:20:56 -0500
Subject: [PATCH 17/40] Reset statusnet_blocking of all fetched users first
 while refreshing block list

---
 src/modules/users.js | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/src/modules/users.js b/src/modules/users.js
index 79046371..dd729410 100644
--- a/src/modules/users.js
+++ b/src/modules/users.js
@@ -102,6 +102,11 @@ export const mutations = {
       }
     })
   },
+  updateBlocks (state, blockedUsers) {
+    // Reset statusnet_blocking of all fetched users
+    each(state.users, (user) => { user.statusnet_blocking = false })
+    each(blockedUsers, (user) => mergeOrAdd(state.users, state.usersObject, user))
+  },
   saveBlockIds (state, blockIds) {
     state.currentUser.blockIds = blockIds
   },
@@ -187,7 +192,7 @@ const users = {
       return store.rootState.api.backendInteractor.fetchBlocks()
         .then((blocks) => {
           store.commit('saveBlockIds', map(blocks, 'id'))
-          store.commit('addNewUsers', blocks)
+          store.commit('updateBlocks', blocks)
           return blocks
         })
     },

From a0bccbce9452c21cc075ecc5ba99c1e99a85dbed Mon Sep 17 00:00:00 2001
From: taehoon <th.dev91@gmail.com>
Date: Sat, 2 Mar 2019 08:21:15 -0500
Subject: [PATCH 18/40] Add comment

---
 src/modules/users.js | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/modules/users.js b/src/modules/users.js
index dd729410..d75ac9d4 100644
--- a/src/modules/users.js
+++ b/src/modules/users.js
@@ -111,6 +111,7 @@ export const mutations = {
     state.currentUser.blockIds = blockIds
   },
   updateMutes (state, mutedUsers) {
+    // Reset muted of all fetched users
     each(state.users, (user) => { user.muted = false })
     // TODO: Remove this line once we get muted property of user object from the api
     const newUsers = map(mutedUsers, (user) => ({ ...user, muted: true }))

From ee94a6732aff8a4ab64ca885e07d53f58bfb4cb4 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Thu, 21 Mar 2019 22:49:26 +0200
Subject: [PATCH 19/40] why did i do that

---
 src/components/conversation/conversation.js | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js
index fd4303ca..32bab144 100644
--- a/src/components/conversation/conversation.js
+++ b/src/components/conversation/conversation.js
@@ -93,6 +93,11 @@ const conversation = {
             statuses.forEach(status => this.relevantIds.push(status.id))
           })
           .then(() => this.setHighlight(this.statusId))
+      } else {
+        const id = this.$route.params.id
+        this.$store.state.api.backendInteractor.fetchStatus({id})
+          .then((status) => this.$store.dispatch('addNewStatuses', { statuses: [status] }))
+          .then(() => this.fetchConversation())
       }
     },
     getReplies (id) {

From d6c62fa50f1992c109c60d03aa44f5ed3ee94284 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Thu, 21 Mar 2019 23:27:14 +0200
Subject: [PATCH 20/40] minor UI improvements - keep current behavior of
 showing originating post initially

---
 src/components/conversation-page/conversation-page.js |  5 ++---
 src/components/conversation/conversation.js           | 11 ++++++++++-
 2 files changed, 12 insertions(+), 4 deletions(-)

diff --git a/src/components/conversation-page/conversation-page.js b/src/components/conversation-page/conversation-page.js
index 8f1ac3d9..1da70ce9 100644
--- a/src/components/conversation-page/conversation-page.js
+++ b/src/components/conversation-page/conversation-page.js
@@ -1,5 +1,4 @@
 import Conversation from '../conversation/conversation.vue'
-import { find } from 'lodash'
 
 const conversationPage = {
   components: {
@@ -8,8 +7,8 @@ const conversationPage = {
   computed: {
     statusoid () {
       const id = this.$route.params.id
-      const statuses = this.$store.state.statuses.allStatuses
-      const status = find(statuses, {id})
+      const statuses = this.$store.state.statuses.allStatusesObject
+      const status = statuses[id]
 
       return status
     }
diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js
index 32bab144..e543102a 100644
--- a/src/components/conversation/conversation.js
+++ b/src/components/conversation/conversation.js
@@ -37,6 +37,15 @@ const conversation = {
     status () {
       return this.statusoid
     },
+    idsToShow () {
+      if (this.relevantIds.length > 0) {
+        return this.relevantIds
+      } else if (this.statusId) {
+        return [this.statusId]
+      } else {
+        return []
+      }
+    },
     statusId () {
       if (this.statusoid.retweeted_status) {
         return this.statusoid.retweeted_status.id
@@ -50,7 +59,7 @@ const conversation = {
       }
 
       const statusesObject = this.$store.state.statuses.allStatusesObject
-      const conversation = this.relevantIds.reduce((acc, id) => {
+      const conversation = this.idsToShow.reduce((acc, id) => {
         acc.push(statusesObject[id])
         return acc
       }, [])

From 67719e9a23da1420e8edbd41265cf6f46995b4b8 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Thu, 21 Mar 2019 23:45:18 +0200
Subject: [PATCH 21/40] less hackery, more direct usage of mastoapi

---
 src/components/conversation/conversation.js | 11 +++++--
 src/services/api/api.service.js             | 32 ++++++++-------------
 2 files changed, 20 insertions(+), 23 deletions(-)

diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js
index e543102a..ff18a9c8 100644
--- a/src/components/conversation/conversation.js
+++ b/src/components/conversation/conversation.js
@@ -1,4 +1,5 @@
 import { reduce, filter } from 'lodash'
+import { set } from 'vue'
 import Status from '../status/status.vue'
 
 const sortById = (a, b) => {
@@ -97,9 +98,13 @@ const conversation = {
       if (this.status) {
         const conversationId = this.status.id
         this.$store.state.api.backendInteractor.fetchConversation({id: conversationId})
-          .then((statuses) => {
-            this.$store.dispatch('addNewStatuses', { statuses })
-            statuses.forEach(status => this.relevantIds.push(status.id))
+          .then(({ancestors, descendants}) => {
+            this.$store.dispatch('addNewStatuses', { statuses: ancestors })
+            this.$store.dispatch('addNewStatuses', { statuses: descendants })
+            set(this, 'relevantIds', [].concat(
+              ancestors.map(_ => _.id),
+              this.statusId,
+              descendants.map(_ => _.id)))
           })
           .then(() => this.setHighlight(this.statusId))
       } else {
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index 1510d146..9f628b13 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -313,27 +313,19 @@ const fetchFollowRequests = ({credentials}) => {
 }
 
 const fetchConversation = ({id, credentials}) => {
-  let url = MASTODON_STATUS_URL(id)
   let urlContext = MASTODON_STATUS_CONTEXT_URL(id)
-  return Promise.all([
-    fetch(url, { headers: authHeaders(credentials) })
-      .then((data) => {
-        if (data.ok) {
-          return data
-        }
-        throw new Error('Error fetching timeline', data)
-      })
-      .then((data) => data.json()),
-    fetch(urlContext, { headers: authHeaders(credentials) })
-      .then((data) => {
-        if (data.ok) {
-          return data
-        }
-        throw new Error('Error fetching timeline', data)
-      })
-      .then((data) => data.json())])
-    .then(([status, context]) => [...context.ancestors, status, ...context.descendants])
-    .then((data) => data.map(parseStatus))
+  return fetch(urlContext, { headers: authHeaders(credentials) })
+    .then((data) => {
+      if (data.ok) {
+        return data
+      }
+      throw new Error('Error fetching timeline', data)
+    })
+    .then((data) => data.json())
+    .then(({ancestors, descendants}) => ({
+      ancestors: ancestors.map(parseStatus),
+      descendants: descendants.map(parseStatus)
+    }))
 }
 
 const fetchStatus = ({id, credentials}) => {

From e617ed285e8ecb13829558197bdf04ffb9583ea9 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Fri, 22 Mar 2019 00:05:20 +0200
Subject: [PATCH 22/40] reactivity fixes

---
 src/modules/statuses.js | 3 ++-
 src/modules/users.js    | 4 ++--
 2 files changed, 4 insertions(+), 3 deletions(-)

diff --git a/src/modules/statuses.js b/src/modules/statuses.js
index f14b8703..a16342e0 100644
--- a/src/modules/statuses.js
+++ b/src/modules/statuses.js
@@ -1,4 +1,5 @@
 import { remove, slice, each, find, maxBy, minBy, merge, first, last, isArray, omitBy } from 'lodash'
+import { set } from 'vue'
 import apiService from '../services/api/api.service.js'
 // import parse from '../services/status_parser/status_parser.js'
 
@@ -82,7 +83,7 @@ const mergeOrAdd = (arr, obj, item) => {
     // This is a new item, prepare it
     prepareStatus(item)
     arr.push(item)
-    obj[item.id] = item
+    set(obj, item.id, item)
     return {item, new: true}
   }
 }
diff --git a/src/modules/users.js b/src/modules/users.js
index 1fe12fc8..2950971f 100644
--- a/src/modules/users.js
+++ b/src/modules/users.js
@@ -16,9 +16,9 @@ export const mergeOrAdd = (arr, obj, item) => {
   } else {
     // This is a new item, prepare it
     arr.push(item)
-    obj[item.id] = item
+    set(obj, item.id, item)
     if (item.screen_name && !item.screen_name.includes('@')) {
-      obj[item.screen_name.toLowerCase()] = item
+      set(obj, item.screen_name.toLowerCase(), item)
     }
     return { item, new: true }
   }

From af85a3f244fd04bc01c5c1f6aa3f1914a86a6ac4 Mon Sep 17 00:00:00 2001
From: taehoon <th.dev91@gmail.com>
Date: Thu, 21 Mar 2019 21:27:10 -0400
Subject: [PATCH 23/40] Switch to mastoapi

---
 src/services/api/api.service.js | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index 73ea15aa..c3f863c8 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -18,8 +18,6 @@ const MENTIONS_URL = '/api/statuses/mentions.json'
 const DM_TIMELINE_URL = '/api/statuses/dm_timeline.json'
 const FOLLOWERS_URL = '/api/statuses/followers.json'
 const FRIENDS_URL = '/api/statuses/friends.json'
-const BLOCKS_URL = '/api/statuses/blocks.json'
-const MUTES_URL = '/api/v1/mutes.json'
 const MUTING_URL = '/api/v1/accounts/:id/mute'
 const UNMUTING_URL = '/api/v1/accounts/:id/unmute'
 const FOLLOWING_URL = '/api/friendships/create.json'
@@ -46,6 +44,8 @@ const MASTODON_USER_FAVORITES_TIMELINE_URL = '/api/v1/favourites'
 const MASTODON_USER_URL = '/api/v1/accounts'
 const MASTODON_USER_RELATIONSHIPS_URL = '/api/v1/accounts/relationships'
 const MASTODON_USER_TIMELINE_URL = id => `/api/v1/accounts/${id}/statuses`
+const MASTODON_USER_BLOCKS_URL = '/api/v1/blocks/'
+const MASTODON_USER_MUTES_URL = '/api/v1/mutes/'
 
 import { each, map } from 'lodash'
 import { parseStatus, parseUser, parseNotification } from '../entity_normalizer/entity_normalizer.service.js'
@@ -530,7 +530,8 @@ const changePassword = ({credentials, password, newPassword, newPasswordConfirma
 }
 
 const fetchMutes = ({credentials}) => {
-  return promisedRequest(MUTES_URL, { headers: authHeaders(credentials) })
+  return promisedRequest(MASTODON_USER_MUTES_URL, { headers: authHeaders(credentials) })
+    .then((users) => users.map(parseUser))
 }
 
 const muteUser = ({id, credentials}) => {
@@ -550,9 +551,8 @@ const unmuteUser = ({id, credentials}) => {
 }
 
 const fetchBlocks = ({credentials}) => {
-  return promisedRequest(BLOCKS_URL, {
-    headers: authHeaders(credentials)
-  })
+  return promisedRequest(MASTODON_USER_BLOCKS_URL, { headers: authHeaders(credentials) })
+    .then((users) => users.map(parseUser))
 }
 
 const fetchOAuthTokens = ({credentials}) => {

From 883a84b31306f2725737ee408900e20b57dbfdf6 Mon Sep 17 00:00:00 2001
From: taehoon <th.dev91@gmail.com>
Date: Thu, 21 Mar 2019 21:30:30 -0400
Subject: [PATCH 24/40] =?UTF-8?q?No=20need=20to=20fetch=20user=20data=20us?=
 =?UTF-8?q?ing=20old=20api=20anymore=20=F0=9F=8E=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/modules/users.js | 5 -----
 1 file changed, 5 deletions(-)

diff --git a/src/modules/users.js b/src/modules/users.js
index d75ac9d4..83c90ea5 100644
--- a/src/modules/users.js
+++ b/src/modules/users.js
@@ -212,11 +212,6 @@ const users = {
     },
     fetchMutes (store) {
       return store.rootState.api.backendInteractor.fetchMutes()
-        .then((mutes) => {
-          // fetchMutes api doesn't return full user data, let's fetch full user data using separate api calls
-          const promises = mutes.map(({ id }) => store.rootState.api.backendInteractor.fetchUser({ id }))
-          return Promise.all(promises)
-        })
         .then((mutes) => {
           store.commit('updateMutes', mutes)
           store.commit('saveMuteIds', map(mutes, 'id'))

From 379e33f6a531fad17477c43e0112941343c737ef Mon Sep 17 00:00:00 2001
From: taehoon <th.dev91@gmail.com>
Date: Thu, 21 Mar 2019 21:31:16 -0400
Subject: [PATCH 25/40] masto api sends muted property now

---
 src/modules/users.js | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/src/modules/users.js b/src/modules/users.js
index 83c90ea5..ca4358ca 100644
--- a/src/modules/users.js
+++ b/src/modules/users.js
@@ -113,9 +113,7 @@ export const mutations = {
   updateMutes (state, mutedUsers) {
     // Reset muted of all fetched users
     each(state.users, (user) => { user.muted = false })
-    // TODO: Remove this line once we get muted property of user object from the api
-    const newUsers = map(mutedUsers, (user) => ({ ...user, muted: true }))
-    each(newUsers, (user) => mergeOrAdd(state.users, state.usersObject, user))
+    each(mutedUsers, (user) => mergeOrAdd(state.users, state.usersObject, user))
   },
   saveMuteIds (state, muteIds) {
     state.currentUser.muteIds = muteIds

From 8702d23a137d06607fc525b6c791abe24ed021ff Mon Sep 17 00:00:00 2001
From: taehoon <th.dev91@gmail.com>
Date: Thu, 21 Mar 2019 21:44:59 -0400
Subject: [PATCH 26/40] switch to mastoapi

---
 src/modules/users.js            |  6 +++---
 src/services/api/api.service.js | 10 ++++------
 2 files changed, 7 insertions(+), 9 deletions(-)

diff --git a/src/modules/users.js b/src/modules/users.js
index ca4358ca..f12348d4 100644
--- a/src/modules/users.js
+++ b/src/modules/users.js
@@ -197,8 +197,8 @@ const users = {
     },
     blockUser (store, userId) {
       return store.rootState.api.backendInteractor.blockUser(userId)
-        .then((blockedUser) => {
-          store.commit('addNewUsers', [blockedUser])
+        .then((relationship) => {
+          store.commit('updateUserRelationship', [relationship])
           store.commit('removeStatus', { timeline: 'friends', userId })
           store.commit('removeStatus', { timeline: 'public', userId })
           store.commit('removeStatus', { timeline: 'publicAndExternal', userId })
@@ -206,7 +206,7 @@ const users = {
     },
     unblockUser (store, id) {
       return store.rootState.api.backendInteractor.unblockUser(id)
-        .then((user) => store.commit('addNewUsers', [user]))
+        .then((relationship) => store.commit('updateUserRelationship', [relationship]))
     },
     fetchMutes (store) {
       return store.rootState.api.backendInteractor.fetchMutes()
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index c3f863c8..36441017 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -30,8 +30,6 @@ const PROFILE_UPDATE_URL = '/api/account/update_profile.json'
 const EXTERNAL_PROFILE_URL = '/api/externalprofile/show.json'
 const QVITTER_USER_NOTIFICATIONS_URL = '/api/qvitter/statuses/notifications.json'
 const QVITTER_USER_NOTIFICATIONS_READ_URL = '/api/qvitter/statuses/notifications/read.json'
-const BLOCKING_URL = '/api/blocks/create.json'
-const UNBLOCKING_URL = '/api/blocks/destroy.json'
 const FOLLOW_IMPORT_URL = '/api/pleroma/follow_import'
 const DELETE_ACCOUNT_URL = '/api/pleroma/delete_account'
 const CHANGE_PASSWORD_URL = '/api/pleroma/change_password'
@@ -46,6 +44,8 @@ const MASTODON_USER_RELATIONSHIPS_URL = '/api/v1/accounts/relationships'
 const MASTODON_USER_TIMELINE_URL = id => `/api/v1/accounts/${id}/statuses`
 const MASTODON_USER_BLOCKS_URL = '/api/v1/blocks/'
 const MASTODON_USER_MUTES_URL = '/api/v1/mutes/'
+const MASTODON_BLOCK_USER_URL = id => `/api/v1/accounts/${id}/block`
+const MASTODON_UNBLOCK_USER_URL = id => `/api/v1/accounts/${id}/unblock`
 
 import { each, map } from 'lodash'
 import { parseStatus, parseUser, parseNotification } from '../entity_normalizer/entity_normalizer.service.js'
@@ -228,16 +228,14 @@ const unfollowUser = ({id, credentials}) => {
 }
 
 const blockUser = ({id, credentials}) => {
-  let url = `${BLOCKING_URL}?user_id=${id}`
-  return fetch(url, {
+  return fetch(MASTODON_BLOCK_USER_URL(id), {
     headers: authHeaders(credentials),
     method: 'POST'
   }).then((data) => data.json())
 }
 
 const unblockUser = ({id, credentials}) => {
-  let url = `${UNBLOCKING_URL}?user_id=${id}`
-  return fetch(url, {
+  return fetch(MASTODON_UNBLOCK_USER_URL(id), {
     headers: authHeaders(credentials),
     method: 'POST'
   }).then((data) => data.json())

From a64e744c1b4aca4bd4ec2ba78a364dd2bbf81b47 Mon Sep 17 00:00:00 2001
From: taehoon <th.dev91@gmail.com>
Date: Thu, 21 Mar 2019 21:53:24 -0400
Subject: [PATCH 27/40] switch to mastoapi

---
 src/modules/users.js            | 12 ++----------
 src/services/api/api.service.js | 11 ++++-------
 src/utils/url.js                |  9 ---------
 3 files changed, 6 insertions(+), 26 deletions(-)
 delete mode 100644 src/utils/url.js

diff --git a/src/modules/users.js b/src/modules/users.js
index f12348d4..bb9a717f 100644
--- a/src/modules/users.js
+++ b/src/modules/users.js
@@ -118,14 +118,6 @@ export const mutations = {
   saveMuteIds (state, muteIds) {
     state.currentUser.muteIds = muteIds
   },
-  muteUser (state, id) {
-    const user = state.usersObject[id]
-    set(user, 'muted', true)
-  },
-  unmuteUser (state, id) {
-    const user = state.usersObject[id]
-    set(user, 'muted', false)
-  },
   setUserForStatus (state, status) {
     status.user = state.usersObject[status.user.id]
   },
@@ -218,11 +210,11 @@ const users = {
     },
     muteUser (store, id) {
       return store.rootState.api.backendInteractor.muteUser(id)
-        .then(() => store.commit('muteUser', id))
+        .then((relationship) => store.commit('updateUserRelationship', [relationship]))
     },
     unmuteUser (store, id) {
       return store.rootState.api.backendInteractor.unmuteUser(id)
-        .then(() => store.commit('unmuteUser', id))
+        .then((relationship) => store.commit('updateUserRelationship', [relationship]))
     },
     addFriends ({ rootState, commit }, fetchBy) {
       return new Promise((resolve, reject) => {
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index 36441017..8586f993 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -18,8 +18,6 @@ const MENTIONS_URL = '/api/statuses/mentions.json'
 const DM_TIMELINE_URL = '/api/statuses/dm_timeline.json'
 const FOLLOWERS_URL = '/api/statuses/followers.json'
 const FRIENDS_URL = '/api/statuses/friends.json'
-const MUTING_URL = '/api/v1/accounts/:id/mute'
-const UNMUTING_URL = '/api/v1/accounts/:id/unmute'
 const FOLLOWING_URL = '/api/friendships/create.json'
 const UNFOLLOWING_URL = '/api/friendships/destroy.json'
 const REGISTRATION_URL = '/api/account/register.json'
@@ -46,12 +44,13 @@ const MASTODON_USER_BLOCKS_URL = '/api/v1/blocks/'
 const MASTODON_USER_MUTES_URL = '/api/v1/mutes/'
 const MASTODON_BLOCK_USER_URL = id => `/api/v1/accounts/${id}/block`
 const MASTODON_UNBLOCK_USER_URL = id => `/api/v1/accounts/${id}/unblock`
+const MASTODON_MUTE_USER_URL = id => `/api/v1/accounts/${id}/mute`
+const MASTODON_UNMUTE_USER_URL = id => `/api/v1/accounts/${id}/unmute`
 
 import { each, map } from 'lodash'
 import { parseStatus, parseUser, parseNotification } from '../entity_normalizer/entity_normalizer.service.js'
 import 'whatwg-fetch'
 import { StatusCodeError } from '../errors/errors'
-import { generateUrl } from '../../utils/url'
 
 const oldfetch = window.fetch
 
@@ -533,16 +532,14 @@ const fetchMutes = ({credentials}) => {
 }
 
 const muteUser = ({id, credentials}) => {
-  const url = generateUrl(MUTING_URL, { id })
-  return promisedRequest(url, {
+  return promisedRequest(MASTODON_MUTE_USER_URL(id), {
     headers: authHeaders(credentials),
     method: 'POST'
   })
 }
 
 const unmuteUser = ({id, credentials}) => {
-  const url = generateUrl(UNMUTING_URL, { id })
-  return promisedRequest(url, {
+  return promisedRequest(MASTODON_UNMUTE_USER_URL(id), {
     headers: authHeaders(credentials),
     method: 'POST'
   })
diff --git a/src/utils/url.js b/src/utils/url.js
deleted file mode 100644
index 79ea7394..00000000
--- a/src/utils/url.js
+++ /dev/null
@@ -1,9 +0,0 @@
-// Generate url based on template
-// Example: /api/v1/accounts/:id/mute -> /api/v1/accounts/123/mute
-export const generateUrl = (template, params = {}) => {
-  let url = template
-  Object.entries(params).forEach(([key, value]) => {
-    url = url.replace(':' + key, value)
-  })
-  return url
-}

From 9fa1bc63b0d95a2cd0868c0356953117e25f0328 Mon Sep 17 00:00:00 2001
From: taehoon <th.dev91@gmail.com>
Date: Thu, 21 Mar 2019 21:58:02 -0400
Subject: [PATCH 28/40] =?UTF-8?q?No=20need=20to=20fetch=20mutes=20on=20loa?=
 =?UTF-8?q?d=20anymore=20=F0=9F=99=8C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/modules/users.js | 5 -----
 1 file changed, 5 deletions(-)

diff --git a/src/modules/users.js b/src/modules/users.js
index bb9a717f..9b7b29e7 100644
--- a/src/modules/users.js
+++ b/src/modules/users.js
@@ -365,11 +365,6 @@ const users = {
               // Start getting fresh posts.
               store.dispatch('startFetching', { timeline: 'friends' })
 
-              // Fetch mutes
-              // TODO: We should not show timeline until fetchMutes is resolved
-              // However, we can get rid of this logic totally if we can know user muted state from user object
-              store.dispatch('fetchMutes')
-
               // Fetch our friends
               store.rootState.api.backendInteractor.fetchFriends({ id: user.id })
                 .then((friends) => commit('addNewUsers', friends))

From ed4540530bf1d327c91af2cf4474646aa80cd0fb Mon Sep 17 00:00:00 2001
From: dave <starpumadev@gmail.com>
Date: Fri, 22 Mar 2019 10:42:21 -0400
Subject: [PATCH 29/40] #451 - add class to username span

---
 src/components/basic_user_card/basic_user_card.vue | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/components/basic_user_card/basic_user_card.vue b/src/components/basic_user_card/basic_user_card.vue
index 4a24e810..8afe8b44 100644
--- a/src/components/basic_user_card/basic_user_card.vue
+++ b/src/components/basic_user_card/basic_user_card.vue
@@ -8,8 +8,8 @@
     </div>
     <div class="basic-user-card-collapsed-content" v-else>
       <div :title="user.name" class="basic-user-card-user-name">
-        <span v-if="user.name_html" v-html="user.name_html"></span>
-        <span v-else>{{ user.name }}</span>
+        <span v-if="user.name_html" class="basic-user-card-user-name-value" v-html="user.name_html"></span>
+        <span v-else class="basic-user-card-user-name-value">{{ user.name }}</span>
       </div>
       <div>
         <router-link class="basic-user-card-screen-name" :to="userProfileLink(user)">
@@ -53,7 +53,7 @@
       vertical-align: middle;
     }
 
-    > span {
+    &-value {
       display: inline-block;
       max-width: 100%;
       overflow: hidden;

From d70928792d34b6da6ef3af3db2d9da7a269c5435 Mon Sep 17 00:00:00 2001
From: dave <starpumadev@gmail.com>
Date: Fri, 22 Mar 2019 13:15:11 -0400
Subject: [PATCH 30/40] #448 - fix timeline fetch error when status text is
 null

---
 src/services/entity_normalizer/entity_normalizer.service.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
index e831963a..57a6adf9 100644
--- a/src/services/entity_normalizer/entity_normalizer.service.js
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -293,5 +293,5 @@ export const parseNotification = (data) => {
 
 const isNsfw = (status) => {
   const nsfwRegex = /#nsfw/i
-  return (status.tags || []).includes('nsfw') || !!status.text.match(nsfwRegex)
+  return (status.tags || []).includes('nsfw') || !!(status.text || '').match(nsfwRegex)
 }

From 31d09931c8a2be1484a5023f0663e16549bc2e7d Mon Sep 17 00:00:00 2001
From: dave <starpumadev@gmail.com>
Date: Fri, 22 Mar 2019 14:04:22 -0400
Subject: [PATCH 31/40] #450 - dispatch login after saved state is loaded

---
 src/lib/persisted_state.js | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/src/lib/persisted_state.js b/src/lib/persisted_state.js
index 7ab89c12..720ff706 100644
--- a/src/lib/persisted_state.js
+++ b/src/lib/persisted_state.js
@@ -60,6 +60,9 @@ export default function createPersistedState ({
             merge({}, store.state, savedState)
           )
         }
+        if (store.state.oauth.token) {
+          store.dispatch('loginUser', store.state.oauth.token)
+        }
         loaded = true
       } catch (e) {
         console.log("Couldn't load state")

From 05634d222d87845f8eb3d022f55e92c9c996ea4a Mon Sep 17 00:00:00 2001
From: nik <nik@telekem.net>
Date: Sat, 23 Mar 2019 22:26:31 -0500
Subject: [PATCH 32/40] remove border radius of suggested emojis

---
 src/components/post_status_form/post_status_form.vue | 2 --
 1 file changed, 2 deletions(-)

diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue
index 3d1df91b..c18b2afd 100644
--- a/src/components/post_status_form/post_status_form.vue
+++ b/src/components/post_status_form/post_status_form.vue
@@ -287,8 +287,6 @@
     img {
       width: 24px;
       height: 24px;
-      border-radius: $fallback--avatarRadius;
-      border-radius: var(--avatarRadius, $fallback--avatarRadius);
       object-fit: contain;
     }
 

From b241539d4df0c3a277723cb308c39a5e9665b7b1 Mon Sep 17 00:00:00 2001
From: nik <nik@telekem.net>
Date: Sat, 23 Mar 2019 22:45:24 -0500
Subject: [PATCH 33/40] prevent text pasting if image is pasted

---
 src/components/post_status_form/post_status_form.js | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js
index 1f0df35a..c5f30ca6 100644
--- a/src/components/post_status_form/post_status_form.js
+++ b/src/components/post_status_form/post_status_form.js
@@ -296,6 +296,8 @@ const PostStatusForm = {
     },
     paste (e) {
       if (e.clipboardData.files.length > 0) {
+        // prevent pasting of file as text
+        e.preventDefault()
         // Strangely, files property gets emptied after event propagation
         // Trying to wrap it in array doesn't work. Plus I doubt it's possible
         // to hold more than one file in clipboard.

From f03747e84124ea45efd16a2f386d7f14996f736a Mon Sep 17 00:00:00 2001
From: taehoon <th.dev91@gmail.com>
Date: Sun, 24 Mar 2019 13:04:47 -0400
Subject: [PATCH 34/40] Revert changes

---
 src/modules/users.js | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/src/modules/users.js b/src/modules/users.js
index 9b7b29e7..f83ae298 100644
--- a/src/modules/users.js
+++ b/src/modules/users.js
@@ -365,6 +365,9 @@ const users = {
               // Start getting fresh posts.
               store.dispatch('startFetching', { timeline: 'friends' })
 
+              // Get user mutes
+              store.dispatch('fetchMutes')
+
               // Fetch our friends
               store.rootState.api.backendInteractor.fetchFriends({ id: user.id })
                 .then((friends) => commit('addNewUsers', friends))

From 9802344603dc6a6341cc4bdb4828e0baddc1b03b Mon Sep 17 00:00:00 2001
From: taehoon <th.dev91@gmail.com>
Date: Fri, 15 Mar 2019 15:02:00 -0400
Subject: [PATCH 35/40] Switch to mastoapi for posting status and uploading
 media

---
 src/components/media_upload/media_upload.js   |  2 +-
 .../post_status_form/post_status_form.js      |  2 +-
 .../post_status_form/post_status_form.vue     |  8 +++----
 src/services/api/api.service.js               | 23 ++++++++++---------
 .../status_poster/status_poster.service.js    | 22 ++----------------
 5 files changed, 20 insertions(+), 37 deletions(-)

diff --git a/src/components/media_upload/media_upload.js b/src/components/media_upload/media_upload.js
index 1c874faa..e4b3d460 100644
--- a/src/components/media_upload/media_upload.js
+++ b/src/components/media_upload/media_upload.js
@@ -20,7 +20,7 @@ const mediaUpload = {
         return
       }
       const formData = new FormData()
-      formData.append('media', file)
+      formData.append('file', file)
 
       self.$emit('uploading')
       self.uploading = true
diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js
index 1f0df35a..142d9d90 100644
--- a/src/components/post_status_form/post_status_form.js
+++ b/src/components/post_status_form/post_status_form.js
@@ -292,7 +292,7 @@ const PostStatusForm = {
       this.submitDisabled = false
     },
     type (fileInfo) {
-      return fileTypeService.fileType(fileInfo.mimetype)
+      return fileTypeService.fileType(fileInfo.pleroma.mime_type)
     },
     paste (e) {
       if (e.clipboardData.files.length > 0) {
diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue
index 3d1df91b..166691c2 100644
--- a/src/components/post_status_form/post_status_form.vue
+++ b/src/components/post_status_form/post_status_form.vue
@@ -84,10 +84,10 @@
         <div class="media-upload-wrapper" v-for="file in newStatus.files">
           <i class="fa button-icon icon-cancel" @click="removeMediaFile(file)"></i>
           <div class="media-upload-container attachment">
-            <img class="thumbnail media-upload" :src="file.image" v-if="type(file) === 'image'"></img>
-            <video v-if="type(file) === 'video'" :src="file.image" controls></video>
-            <audio v-if="type(file) === 'audio'" :src="file.image" controls></audio>
-            <a v-if="type(file) === 'unknown'" :href="file.image">{{file.url}}</a>
+            <img class="thumbnail media-upload" :src="file.url" v-if="type(file) === 'image'"></img>
+            <video v-if="type(file) === 'video'" :src="file.url" controls></video>
+            <audio v-if="type(file) === 'audio'" :src="file.url" controls></audio>
+            <a v-if="type(file) === 'unknown'" :href="file.url">{{file.url}}</a>
           </div>
         </div>
       </div>
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index 8586f993..a15cecaf 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -9,10 +9,8 @@ const FAVORITE_URL = '/api/favorites/create'
 const UNFAVORITE_URL = '/api/favorites/destroy'
 const RETWEET_URL = '/api/statuses/retweet'
 const UNRETWEET_URL = '/api/statuses/unretweet'
-const STATUS_UPDATE_URL = '/api/statuses/update.json'
 const STATUS_DELETE_URL = '/api/statuses/destroy'
 const STATUS_URL = '/api/statuses/show'
-const MEDIA_UPLOAD_URL = '/api/statusnet/media/upload'
 const CONVERSATION_URL = '/api/statusnet/conversation'
 const MENTIONS_URL = '/api/statuses/mentions.json'
 const DM_TIMELINE_URL = '/api/statuses/dm_timeline.json'
@@ -46,6 +44,8 @@ const MASTODON_BLOCK_USER_URL = id => `/api/v1/accounts/${id}/block`
 const MASTODON_UNBLOCK_USER_URL = id => `/api/v1/accounts/${id}/unblock`
 const MASTODON_MUTE_USER_URL = id => `/api/v1/accounts/${id}/mute`
 const MASTODON_UNMUTE_USER_URL = id => `/api/v1/accounts/${id}/unmute`
+const MASTODON_POST_STATUS_URL = '/api/v1/statuses'
+const MASTODON_MEDIA_UPLOAD_URL = '/api/v1/media'
 
 import { each, map } from 'lodash'
 import { parseStatus, parseUser, parseNotification } from '../entity_normalizer/entity_normalizer.service.js'
@@ -439,23 +439,25 @@ const unretweet = ({ id, credentials }) => {
   })
 }
 
-const postStatus = ({credentials, status, spoilerText, visibility, sensitive, mediaIds, inReplyToStatusId, contentType, noAttachmentLinks}) => {
-  const idsText = mediaIds.join(',')
+const postStatus = ({credentials, status, spoilerText, visibility, sensitive, mediaIds, inReplyToStatusId, contentType}) => {
   const form = new FormData()
 
   form.append('status', status)
   form.append('source', 'Pleroma FE')
-  if (noAttachmentLinks) form.append('no_attachment_links', noAttachmentLinks)
   if (spoilerText) form.append('spoiler_text', spoilerText)
   if (visibility) form.append('visibility', visibility)
   if (sensitive) form.append('sensitive', sensitive)
   if (contentType) form.append('content_type', contentType)
-  form.append('media_ids', idsText)
+  if (mediaIds) {
+    mediaIds.forEach(val => {
+      form.append('media_ids[]', val)
+    })
+  }
   if (inReplyToStatusId) {
-    form.append('in_reply_to_status_id', inReplyToStatusId)
+    form.append('in_reply_to_id', inReplyToStatusId)
   }
 
-  return fetch(STATUS_UPDATE_URL, {
+  return fetch(MASTODON_POST_STATUS_URL, {
     body: form,
     method: 'POST',
     headers: authHeaders(credentials)
@@ -480,13 +482,12 @@ const deleteStatus = ({ id, credentials }) => {
 }
 
 const uploadMedia = ({formData, credentials}) => {
-  return fetch(MEDIA_UPLOAD_URL, {
+  return fetch(MASTODON_MEDIA_UPLOAD_URL, {
     body: formData,
     method: 'POST',
     headers: authHeaders(credentials)
   })
-    .then((response) => response.text())
-    .then((text) => (new DOMParser()).parseFromString(text, 'application/xml'))
+    .then((response) => response.json())
 }
 
 const followImport = ({params, credentials}) => {
diff --git a/src/services/status_poster/status_poster.service.js b/src/services/status_poster/status_poster.service.js
index f1932bb6..e70b0f26 100644
--- a/src/services/status_poster/status_poster.service.js
+++ b/src/services/status_poster/status_poster.service.js
@@ -4,7 +4,7 @@ import apiService from '../api/api.service.js'
 const postStatus = ({ store, status, spoilerText, visibility, sensitive, media = [], inReplyToStatusId = undefined, contentType = 'text/plain' }) => {
   const mediaIds = map(media, 'id')
 
-  return apiService.postStatus({credentials: store.state.users.currentUser.credentials, status, spoilerText, visibility, sensitive, mediaIds, inReplyToStatusId, contentType, noAttachmentLinks: store.state.instance.noAttachmentLinks})
+  return apiService.postStatus({credentials: store.state.users.currentUser.credentials, status, spoilerText, visibility, sensitive, mediaIds, inReplyToStatusId, contentType})
     .then((data) => {
       if (!data.error) {
         store.dispatch('addNewStatuses', {
@@ -26,25 +26,7 @@ const postStatus = ({ store, status, spoilerText, visibility, sensitive, media =
 const uploadMedia = ({ store, formData }) => {
   const credentials = store.state.users.currentUser.credentials
 
-  return apiService.uploadMedia({ credentials, formData }).then((xml) => {
-    // Firefox and Chrome treat method differently...
-    let link = xml.getElementsByTagName('link')
-
-    if (link.length === 0) {
-      link = xml.getElementsByTagName('atom:link')
-    }
-
-    link = link[0]
-
-    const mediaData = {
-      id: xml.getElementsByTagName('media_id')[0].textContent,
-      url: xml.getElementsByTagName('media_url')[0].textContent,
-      image: link.getAttribute('href'),
-      mimetype: link.getAttribute('type')
-    }
-
-    return mediaData
-  })
+  return apiService.uploadMedia({ credentials, formData })
 }
 
 const statusPosterService = {

From 966add1b2996b87019051d8c924edb4af0bafb80 Mon Sep 17 00:00:00 2001
From: taehoon <th.dev91@gmail.com>
Date: Sun, 17 Mar 2019 23:22:54 -0400
Subject: [PATCH 36/40] Set default parameter

---
 src/services/api/api.service.js | 15 +++++++--------
 1 file changed, 7 insertions(+), 8 deletions(-)

diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index a15cecaf..079462f7 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -48,7 +48,7 @@ const MASTODON_POST_STATUS_URL = '/api/v1/statuses'
 const MASTODON_MEDIA_UPLOAD_URL = '/api/v1/media'
 
 import { each, map } from 'lodash'
-import { parseStatus, parseUser, parseNotification } from '../entity_normalizer/entity_normalizer.service.js'
+import { parseStatus, parseUser, parseNotification, parseAttachment } from '../entity_normalizer/entity_normalizer.service.js'
 import 'whatwg-fetch'
 import { StatusCodeError } from '../errors/errors'
 
@@ -439,7 +439,7 @@ const unretweet = ({ id, credentials }) => {
   })
 }
 
-const postStatus = ({credentials, status, spoilerText, visibility, sensitive, mediaIds, inReplyToStatusId, contentType}) => {
+const postStatus = ({credentials, status, spoilerText, visibility, sensitive, mediaIds = [], inReplyToStatusId, contentType}) => {
   const form = new FormData()
 
   form.append('status', status)
@@ -448,11 +448,9 @@ const postStatus = ({credentials, status, spoilerText, visibility, sensitive, me
   if (visibility) form.append('visibility', visibility)
   if (sensitive) form.append('sensitive', sensitive)
   if (contentType) form.append('content_type', contentType)
-  if (mediaIds) {
-    mediaIds.forEach(val => {
-      form.append('media_ids[]', val)
-    })
-  }
+  mediaIds.forEach(val => {
+    form.append('media_ids[]', val)
+  })
   if (inReplyToStatusId) {
     form.append('in_reply_to_id', inReplyToStatusId)
   }
@@ -487,7 +485,8 @@ const uploadMedia = ({formData, credentials}) => {
     method: 'POST',
     headers: authHeaders(credentials)
   })
-    .then((response) => response.json())
+    .then((data) => data.json())
+    .then((data) => parseAttachment(data))
 }
 
 const followImport = ({params, credentials}) => {

From 909c315a44a5c6272f000325e5e9c8b33e75f873 Mon Sep 17 00:00:00 2001
From: taehoon <th.dev91@gmail.com>
Date: Sun, 17 Mar 2019 23:23:59 -0400
Subject: [PATCH 37/40] Get correct mimetype through entity_normalizer

---
 src/components/post_status_form/post_status_form.js         | 2 +-
 src/services/entity_normalizer/entity_normalizer.service.js | 5 ++---
 2 files changed, 3 insertions(+), 4 deletions(-)

diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js
index 142d9d90..1f0df35a 100644
--- a/src/components/post_status_form/post_status_form.js
+++ b/src/components/post_status_form/post_status_form.js
@@ -292,7 +292,7 @@ const PostStatusForm = {
       this.submitDisabled = false
     },
     type (fileInfo) {
-      return fileTypeService.fileType(fileInfo.pleroma.mime_type)
+      return fileTypeService.fileType(fileInfo.mimetype)
     },
     paste (e) {
       if (e.clipboardData.files.length > 0) {
diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
index 57a6adf9..0d653207 100644
--- a/src/services/entity_normalizer/entity_normalizer.service.js
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -128,13 +128,12 @@ export const parseUser = (data) => {
   return output
 }
 
-const parseAttachment = (data) => {
+export const parseAttachment = (data) => {
   const output = {}
   const masto = !data.hasOwnProperty('oembed')
 
   if (masto) {
-    // Not exactly same...
-    output.mimetype = data.type
+    output.mimetype = data.pleroma.mime_type
     output.meta = data.meta // not present in BE yet
   } else {
     output.mimetype = data.mimetype

From 6fdbc182ca722dbb12d92e4bf26e841ccda8303d Mon Sep 17 00:00:00 2001
From: taehoon <th.dev91@gmail.com>
Date: Sun, 17 Mar 2019 23:42:07 -0400
Subject: [PATCH 38/40] Add fallback for attachments uploaded via the other
 platforms

---
 src/services/entity_normalizer/entity_normalizer.service.js | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
index 0d653207..35c28ec0 100644
--- a/src/services/entity_normalizer/entity_normalizer.service.js
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -133,7 +133,8 @@ export const parseAttachment = (data) => {
   const masto = !data.hasOwnProperty('oembed')
 
   if (masto) {
-    output.mimetype = data.pleroma.mime_type
+    // Not exactly same...
+    output.mimetype = data.pleroma ? data.pleroma.mime_type : data.type
     output.meta = data.meta // not present in BE yet
   } else {
     output.mimetype = data.mimetype

From 932652e335d19cc6f97fae1a9b12501ffdb9ec0b Mon Sep 17 00:00:00 2001
From: taehoon <th.dev91@gmail.com>
Date: Mon, 25 Mar 2019 12:19:33 -0400
Subject: [PATCH 39/40] Update attachment normalizer

---
 src/services/entity_normalizer/entity_normalizer.service.js | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
index 35c28ec0..5cac3463 100644
--- a/src/services/entity_normalizer/entity_normalizer.service.js
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -136,6 +136,7 @@ export const parseAttachment = (data) => {
     // Not exactly same...
     output.mimetype = data.pleroma ? data.pleroma.mime_type : data.type
     output.meta = data.meta // not present in BE yet
+    output.id = data.id
   } else {
     output.mimetype = data.mimetype
     // output.meta = ??? missing

From cb122e3a99cbb5c3b76b9c5b15a9dd69ef3ed323 Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Mon, 25 Mar 2019 20:11:06 +0200
Subject: [PATCH 40/40] review

---
 src/components/conversation/conversation.js | 11 +++++------
 1 file changed, 5 insertions(+), 6 deletions(-)

diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js
index ff18a9c8..e806be8e 100644
--- a/src/components/conversation/conversation.js
+++ b/src/components/conversation/conversation.js
@@ -27,7 +27,7 @@ const conversation = {
   data () {
     return {
       highlight: null,
-      relevantIds: []
+      converationStatusIds: []
     }
   },
   props: [
@@ -39,8 +39,8 @@ const conversation = {
       return this.statusoid
     },
     idsToShow () {
-      if (this.relevantIds.length > 0) {
-        return this.relevantIds
+      if (this.converationStatusIds.length > 0) {
+        return this.converationStatusIds
       } else if (this.statusId) {
         return [this.statusId]
       } else {
@@ -96,12 +96,11 @@ const conversation = {
   methods: {
     fetchConversation () {
       if (this.status) {
-        const conversationId = this.status.id
-        this.$store.state.api.backendInteractor.fetchConversation({id: conversationId})
+        this.$store.state.api.backendInteractor.fetchConversation({id: this.status.id})
           .then(({ancestors, descendants}) => {
             this.$store.dispatch('addNewStatuses', { statuses: ancestors })
             this.$store.dispatch('addNewStatuses', { statuses: descendants })
-            set(this, 'relevantIds', [].concat(
+            set(this, 'converationStatusIds', [].concat(
               ancestors.map(_ => _.id),
               this.statusId,
               descendants.map(_ => _.id)))