From 457290e81ec9a37bf848f5d166fc77bf487e834d Mon Sep 17 00:00:00 2001
From: rinpatch <rinpatch@sdf.org>
Date: Tue, 3 Sep 2019 19:59:28 +0300
Subject: [PATCH 01/19] Replace `/api/externalprofile/show.json` with a
 MastoAPI equialent

`/api/v1/accounts/:id` supports remote nicknames since pleroma!1622
---
 src/components/who_to_follow/who_to_follow.js          |  2 +-
 .../who_to_follow_panel/who_to_follow_panel.js         |  2 +-
 src/services/api/api.service.js                        | 10 ----------
 .../backend_interactor_service.js                      |  3 ---
 4 files changed, 2 insertions(+), 15 deletions(-)

diff --git a/src/components/who_to_follow/who_to_follow.js b/src/components/who_to_follow/who_to_follow.js
index 8fab6c4d..1aa3a4cd 100644
--- a/src/components/who_to_follow/who_to_follow.js
+++ b/src/components/who_to_follow/who_to_follow.js
@@ -26,7 +26,7 @@ const WhoToFollow = {
         }
         this.users.push(user)
 
-        this.$store.state.api.backendInteractor.externalProfile(user.screen_name)
+        this.$store.state.api.backendInteractor.fetchUser({ id: user.screen_name })
           .then((externalUser) => {
             if (!externalUser.error) {
               this.$store.commit('addNewUsers', [externalUser])
diff --git a/src/components/who_to_follow_panel/who_to_follow_panel.js b/src/components/who_to_follow_panel/who_to_follow_panel.js
index 7d01678b..dcb56106 100644
--- a/src/components/who_to_follow_panel/who_to_follow_panel.js
+++ b/src/components/who_to_follow_panel/who_to_follow_panel.js
@@ -13,7 +13,7 @@ function showWhoToFollow (panel, reply) {
     toFollow.img = img
     toFollow.name = name
 
-    panel.$store.state.api.backendInteractor.externalProfile(name)
+    panel.$store.state.api.backendInteractor.fetchUser({ id: name })
       .then((externalUser) => {
         if (!externalUser.error) {
           panel.$store.commit('addNewUsers', [externalUser])
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index 4cf41e61..887d7d7a 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -4,7 +4,6 @@ import 'whatwg-fetch'
 import { RegistrationError, StatusCodeError } from '../errors/errors'
 
 /* eslint-env browser */
-const EXTERNAL_PROFILE_URL = '/api/externalprofile/show.json'
 const QVITTER_USER_NOTIFICATIONS_READ_URL = '/api/qvitter/statuses/notifications/read.json'
 const BLOCKS_IMPORT_URL = '/api/pleroma/blocks_import'
 const FOLLOW_IMPORT_URL = '/api/pleroma/follow_import'
@@ -220,14 +219,6 @@ const authHeaders = (accessToken) => {
   }
 }
 
-const externalProfile = ({ profileUrl, credentials }) => {
-  let url = `${EXTERNAL_PROFILE_URL}?profileurl=${profileUrl}`
-  return fetch(url, {
-    headers: authHeaders(credentials),
-    method: 'GET'
-  }).then((data) => data.json())
-}
-
 const followUser = ({ id, credentials }) => {
   let url = MASTODON_FOLLOW_URL(id)
   return fetch(url, {
@@ -966,7 +957,6 @@ const apiService = {
   updateBg,
   updateProfile,
   updateBanner,
-  externalProfile,
   importBlocks,
   importFollows,
   deleteAccount,
diff --git a/src/services/backend_interactor_service/backend_interactor_service.js b/src/services/backend_interactor_service/backend_interactor_service.js
index 846d9415..3c44a10c 100644
--- a/src/services/backend_interactor_service/backend_interactor_service.js
+++ b/src/services/backend_interactor_service/backend_interactor_service.js
@@ -127,8 +127,6 @@ const backendInteractorService = credentials => {
   const updateBanner = ({ banner }) => apiService.updateBanner({ credentials, banner })
   const updateProfile = ({ params }) => apiService.updateProfile({ credentials, params })
 
-  const externalProfile = (profileUrl) => apiService.externalProfile({ profileUrl, credentials })
-
   const importBlocks = (file) => apiService.importBlocks({ file, credentials })
   const importFollows = (file) => apiService.importFollows({ file, credentials })
 
@@ -194,7 +192,6 @@ const backendInteractorService = credentials => {
     updateBg,
     updateBanner,
     updateProfile,
-    externalProfile,
     importBlocks,
     importFollows,
     deleteAccount,

From cdbb8ca8b925c0803513b26a15ff927d79fa0316 Mon Sep 17 00:00:00 2001
From: taehoon <th.dev91@gmail.com>
Date: Tue, 3 Sep 2019 16:44:51 -0400
Subject: [PATCH 02/19] force img updating immediately

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

diff --git a/src/components/still-image/still-image.vue b/src/components/still-image/still-image.vue
index 3fff63f9..5a6529f2 100644
--- a/src/components/still-image/still-image.vue
+++ b/src/components/still-image/still-image.vue
@@ -9,6 +9,7 @@
     />
     <img
       ref="src"
+      :key="src"
       :src="src"
       :referrerpolicy="referrerpolicy"
       @load="onLoad"

From d194431642b48f9bd4d66bcce5f187d6d4ff08e2 Mon Sep 17 00:00:00 2001
From: taehoon <th.dev91@gmail.com>
Date: Wed, 4 Sep 2019 11:23:47 -0400
Subject: [PATCH 03/19] add a comment

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

diff --git a/src/components/still-image/still-image.vue b/src/components/still-image/still-image.vue
index 5a6529f2..4137bd59 100644
--- a/src/components/still-image/still-image.vue
+++ b/src/components/still-image/still-image.vue
@@ -7,6 +7,7 @@
       v-if="animated"
       ref="canvas"
     />
+    <!-- NOTE: key is required to force to re-render img tag when src is changed -->
     <img
       ref="src"
       :key="src"

From 424d4ab57e13a8a271a16e063ce70af98759f8f9 Mon Sep 17 00:00:00 2001
From: rinpatch <rinpatch@sdf.org>
Date: Thu, 5 Sep 2019 11:16:11 +0300
Subject: [PATCH 04/19] Utilize `user.requested` to display follow request
 status on user card

Closes #635
---
 src/components/user_card/user_card.js               |  4 +---
 src/components/user_card/user_card.vue              |  4 ++--
 src/services/follow_manipulate/follow_manipulate.js | 11 ++++-------
 3 files changed, 7 insertions(+), 12 deletions(-)

diff --git a/src/components/user_card/user_card.js b/src/components/user_card/user_card.js
index 82d3b835..e3bd7697 100644
--- a/src/components/user_card/user_card.js
+++ b/src/components/user_card/user_card.js
@@ -11,7 +11,6 @@ export default {
   data () {
     return {
       followRequestInProgress: false,
-      followRequestSent: false,
       hideUserStatsLocal: typeof this.$store.state.config.hideUserStats === 'undefined'
         ? this.$store.state.instance.hideUserStats
         : this.$store.state.config.hideUserStats,
@@ -112,9 +111,8 @@ export default {
     followUser () {
       const store = this.$store
       this.followRequestInProgress = true
-      requestFollow(this.user, store).then(({ sent }) => {
+      requestFollow(this.user, store).then(() => {
         this.followRequestInProgress = false
-        this.followRequestSent = sent
       })
     },
     unfollowUser () {
diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue
index fc18e240..0b83cf16 100644
--- a/src/components/user_card/user_card.vue
+++ b/src/components/user_card/user_card.vue
@@ -135,13 +135,13 @@
             <button
               class="btn btn-default btn-block"
               :disabled="followRequestInProgress"
-              :title="followRequestSent ? $t('user_card.follow_again') : ''"
+              :title="user.requested ? $t('user_card.follow_again') : ''"
               @click="followUser"
             >
               <template v-if="followRequestInProgress">
                 {{ $t('user_card.follow_progress') }}
               </template>
-              <template v-else-if="followRequestSent">
+              <template v-else-if="user.requested">
                 {{ $t('user_card.follow_sent') }}
               </template>
               <template v-else>
diff --git a/src/services/follow_manipulate/follow_manipulate.js b/src/services/follow_manipulate/follow_manipulate.js
index 529fdb9b..d82ce593 100644
--- a/src/services/follow_manipulate/follow_manipulate.js
+++ b/src/services/follow_manipulate/follow_manipulate.js
@@ -9,10 +9,7 @@ const fetchUser = (attempt, user, store) => new Promise((resolve, reject) => {
   if (!following && !(locked && sent) && attempt <= 3) {
     // If we BE reports that we still not following that user - retry,
     // increment attempts by one
-    return fetchUser(++attempt, user, store)
-  } else {
-    // If we run out of attempts, just return whatever status is.
-    return sent
+    fetchUser(++attempt, user, store)
   }
 })
 
@@ -23,7 +20,7 @@ export const requestFollow = (user, store) => new Promise((resolve, reject) => {
 
       if (updated.following || (user.locked && user.requested)) {
         // If we get result immediately or the account is locked, just stop.
-        resolve({ sent: updated.requested })
+        resolve()
         return
       }
 
@@ -35,8 +32,8 @@ export const requestFollow = (user, store) => new Promise((resolve, reject) => {
       // Recursive Promise, it will call itself up to 3 times.
 
       return fetchUser(1, user, store)
-        .then((sent) => {
-          resolve({ sent })
+        .then(() => {
+          resolve()
         })
     })
 })

From 8ee5abb1a532bcfb9c70f1dad8cdeefcaf31e59c Mon Sep 17 00:00:00 2001
From: Eugenij <eugenijm@protonmail.com>
Date: Thu, 5 Sep 2019 11:23:28 +0000
Subject: [PATCH 05/19] Password reset page

---
 src/boot/after_store.js                       |   1 +
 src/boot/routes.js                            |   2 +
 src/components/login_form/login_form.vue      |   5 +
 .../password_reset/password_reset.js          |  62 ++++++++++
 .../password_reset/password_reset.vue         | 116 ++++++++++++++++++
 src/i18n/en.json                              |  11 ++
 src/i18n/ru.json                              |  11 ++
 src/services/new_api/password_reset.js        |  18 +++
 8 files changed, 226 insertions(+)
 create mode 100644 src/components/password_reset/password_reset.js
 create mode 100644 src/components/password_reset/password_reset.vue
 create mode 100644 src/services/new_api/password_reset.js

diff --git a/src/boot/after_store.js b/src/boot/after_store.js
index 5a94194c..5cb2acba 100644
--- a/src/boot/after_store.js
+++ b/src/boot/after_store.js
@@ -246,6 +246,7 @@ const getNodeInfo = async ({ store }) => {
       store.dispatch('setInstanceOption', { name: 'gopherAvailable', value: features.includes('gopher') })
       store.dispatch('setInstanceOption', { name: 'pollsAvailable', value: features.includes('polls') })
       store.dispatch('setInstanceOption', { name: 'pollLimits', value: metadata.pollLimits })
+      store.dispatch('setInstanceOption', { name: 'mailerEnabled', value: metadata.mailerEnabled })
 
       store.dispatch('setInstanceOption', { name: 'restrictedNicknames', value: metadata.restrictedNicknames })
       store.dispatch('setInstanceOption', { name: 'postFormats', value: metadata.postFormats })
diff --git a/src/boot/routes.js b/src/boot/routes.js
index 7dc4b2a5..cd02711c 100644
--- a/src/boot/routes.js
+++ b/src/boot/routes.js
@@ -9,6 +9,7 @@ import UserProfile from 'components/user_profile/user_profile.vue'
 import Search from 'components/search/search.vue'
 import Settings from 'components/settings/settings.vue'
 import Registration from 'components/registration/registration.vue'
+import PasswordReset from 'components/password_reset/password_reset.vue'
 import UserSettings from 'components/user_settings/user_settings.vue'
 import FollowRequests from 'components/follow_requests/follow_requests.vue'
 import OAuthCallback from 'components/oauth_callback/oauth_callback.vue'
@@ -46,6 +47,7 @@ export default (store) => {
     { name: 'dms', path: '/users/:username/dms', component: DMs, beforeEnter: validateAuthenticatedRoute },
     { name: 'settings', path: '/settings', component: Settings },
     { name: 'registration', path: '/registration', component: Registration },
+    { name: 'password-reset', path: '/password-reset', component: PasswordReset },
     { name: 'registration-token', path: '/registration/:token', component: Registration },
     { name: 'friend-requests', path: '/friend-requests', component: FollowRequests, beforeEnter: validateAuthenticatedRoute },
     { name: 'user-settings', path: '/user-settings', component: UserSettings, beforeEnter: validateAuthenticatedRoute },
diff --git a/src/components/login_form/login_form.vue b/src/components/login_form/login_form.vue
index 3ec7fe0c..b4fdcefb 100644
--- a/src/components/login_form/login_form.vue
+++ b/src/components/login_form/login_form.vue
@@ -33,6 +33,11 @@
               type="password"
             >
           </div>
+          <div class="form-group">
+            <router-link :to="{name: 'password-reset'}">
+              {{ $t('password_reset.forgot_password') }}
+            </router-link>
+          </div>
         </template>
 
         <div
diff --git a/src/components/password_reset/password_reset.js b/src/components/password_reset/password_reset.js
new file mode 100644
index 00000000..fa71e07a
--- /dev/null
+++ b/src/components/password_reset/password_reset.js
@@ -0,0 +1,62 @@
+import { mapState } from 'vuex'
+import passwordResetApi from '../../services/new_api/password_reset.js'
+
+const passwordReset = {
+  data: () => ({
+    user: {
+      email: ''
+    },
+    isPending: false,
+    success: false,
+    throttled: false,
+    error: null
+  }),
+  computed: {
+    ...mapState({
+      signedIn: (state) => !!state.users.currentUser,
+      instance: state => state.instance
+    }),
+    mailerEnabled () {
+      return this.instance.mailerEnabled
+    }
+  },
+  created () {
+    if (this.signedIn) {
+      this.$router.push({ name: 'root' })
+    }
+  },
+  methods: {
+    dismissError () {
+      this.error = null
+    },
+    submit () {
+      this.isPending = true
+      const email = this.user.email
+      const instance = this.instance.server
+
+      passwordResetApi({ instance, email }).then(({ status }) => {
+        this.isPending = false
+        this.user.email = ''
+
+        if (status === 204) {
+          this.success = true
+          this.error = null
+        } else if (status === 404 || status === 400) {
+          this.error = this.$t('password_reset.not_found')
+          this.$nextTick(() => {
+            this.$refs.email.focus()
+          })
+        } else if (status === 429) {
+          this.throttled = true
+          this.error = this.$t('password_reset.too_many_requests')
+        }
+      }).catch(() => {
+        this.isPending = false
+        this.user.email = ''
+        this.error = this.$t('general.generic_error')
+      })
+    }
+  }
+}
+
+export default passwordReset
diff --git a/src/components/password_reset/password_reset.vue b/src/components/password_reset/password_reset.vue
new file mode 100644
index 00000000..00474e95
--- /dev/null
+++ b/src/components/password_reset/password_reset.vue
@@ -0,0 +1,116 @@
+<template>
+  <div class="settings panel panel-default">
+    <div class="panel-heading">
+      {{ $t('password_reset.password_reset') }}
+    </div>
+    <div class="panel-body">
+      <form
+        class="password-reset-form"
+        @submit.prevent="submit"
+      >
+        <div class="container">
+          <div v-if="!mailerEnabled">
+            <p>
+              {{ $t('password_reset.password_reset_disabled') }}
+            </p>
+          </div>
+          <div v-else-if="success || throttled">
+            <p v-if="success">
+              {{ $t('password_reset.check_email') }}
+            </p>
+            <div class="form-group text-center">
+              <router-link :to="{name: 'root'}">
+                {{ $t('password_reset.return_home') }}
+              </router-link>
+            </div>
+          </div>
+          <div v-else>
+            <p>
+              {{ $t('password_reset.instruction') }}
+            </p>
+            <div class="form-group">
+              <input
+                ref="email"
+                v-model="user.email"
+                :disabled="isPending"
+                :placeholder="$t('password_reset.placeholder')"
+                class="form-control"
+                type="input"
+              >
+            </div>
+            <div class="form-group">
+              <button
+                :disabled="isPending"
+                type="submit"
+                class="btn btn-default btn-block"
+              >
+                {{ $t('general.submit') }}
+              </button>
+            </div>
+          </div>
+          <p
+            v-if="error"
+            class="alert error notice-dismissible"
+          >
+            <span>{{ error }}</span>
+            <a
+              class="button-icon dismiss"
+              @click.prevent="dismissError()"
+            >
+              <i class="icon-cancel" />
+            </a>
+          </p>
+        </div>
+      </form>
+    </div>
+  </div>
+</template>
+
+<script src="./password_reset.js"></script>
+<style lang="scss">
+@import '../../_variables.scss';
+
+.password-reset-form {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  margin: 0.6em;
+
+  .container {
+    display: flex;
+    flex: 1 0;
+    flex-direction: column;
+    margin-top: 0.6em;
+    max-width: 18rem;
+  }
+
+  .form-group {
+    display: flex;
+    flex-direction: column;
+    margin-bottom: 1em;
+    padding: 0.3em 0.0em 0.3em;
+    line-height: 24px;
+  }
+
+  .error {
+    text-align: center;
+    animation-name: shakeError;
+    animation-duration: 0.4s;
+    animation-timing-function: ease-in-out;
+  }
+
+  .alert {
+    padding: 0.5em;
+    margin: 0.3em 0.0em 1em;
+  }
+
+  .notice-dismissible {
+    padding-right: 2rem;
+  }
+
+  .icon-cancel {
+    cursor: pointer;
+  }
+}
+
+</style>
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 6a9af55c..ddde471a 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -608,5 +608,16 @@
     "person_talking": "{count} person talking",
     "people_talking": "{count} people talking",
     "no_results": "No results"
+  },
+  "password_reset": {
+    "forgot_password": "Forgot password?",
+    "password_reset": "Password reset",
+    "instruction": "Enter your email address or username. We will send you a link to reset your password.",
+    "placeholder": "Your email or username",
+    "check_email": "Check your email for a link to reset your password.",
+    "return_home": "Return to the home page",
+    "not_found": "We couldn't find that email or username.",
+    "too_many_requests": "You have reached the limit of attempts, try again later.",
+    "password_reset_disabled": "Password reset is disabled. Please contact your instance administrator."
   }
 }
diff --git a/src/i18n/ru.json b/src/i18n/ru.json
index 90ed6664..3af65f40 100644
--- a/src/i18n/ru.json
+++ b/src/i18n/ru.json
@@ -389,5 +389,16 @@
     "person_talking": "Популярно у {count} человека",
     "people_talking": "Популярно у {count} человек",
     "no_results": "Ничего не найдено"
+  },
+  "password_reset": {
+    "forgot_password": "Забыли пароль?",
+    "password_reset": "Сброс пароля",
+    "instruction": "Введите ваш email или имя пользователя, и мы отправим вам ссылку для сброса пароля.",
+    "placeholder": "Ваш email или имя пользователя",
+    "check_email": "Проверьте ваш email и перейдите по ссылке для сброса пароля.",
+    "return_home": "Вернуться на главную страницу",
+    "not_found": "Мы не смогли найти аккаунт с таким email-ом или именем пользователя.",
+    "too_many_requests": "Вы исчерпали допустимое количество попыток, попробуйте позже.",
+    "password_reset_disabled": "Сброс пароля отключен. Cвяжитесь с администратором вашего сервера."
   }
 }
diff --git a/src/services/new_api/password_reset.js b/src/services/new_api/password_reset.js
new file mode 100644
index 00000000..43199625
--- /dev/null
+++ b/src/services/new_api/password_reset.js
@@ -0,0 +1,18 @@
+import { reduce } from 'lodash'
+
+const MASTODON_PASSWORD_RESET_URL = `/auth/password`
+
+const resetPassword = ({ instance, email }) => {
+  const params = { email }
+  const query = reduce(params, (acc, v, k) => {
+    const encoded = `${k}=${encodeURIComponent(v)}`
+    return `${acc}&${encoded}`
+  }, '')
+  const url = `${instance}${MASTODON_PASSWORD_RESET_URL}?${query}`
+
+  return window.fetch(url, {
+    method: 'POST'
+  })
+}
+
+export default resetPassword

From d7c68d408f07e997457798d4e8902338877f4223 Mon Sep 17 00:00:00 2001
From: taehoon <th.dev91@gmail.com>
Date: Fri, 30 Aug 2019 11:47:15 -0400
Subject: [PATCH 06/19] accept status id instead of status obj as statusoid
 prop

---
 .../conversation-page/conversation-page.js      |  8 ++------
 .../conversation-page/conversation-page.vue     |  2 +-
 src/components/conversation/conversation.js     | 17 ++++++++---------
 3 files changed, 11 insertions(+), 16 deletions(-)

diff --git a/src/components/conversation-page/conversation-page.js b/src/components/conversation-page/conversation-page.js
index 1da70ce9..8f996be1 100644
--- a/src/components/conversation-page/conversation-page.js
+++ b/src/components/conversation-page/conversation-page.js
@@ -5,12 +5,8 @@ const conversationPage = {
     Conversation
   },
   computed: {
-    statusoid () {
-      const id = this.$route.params.id
-      const statuses = this.$store.state.statuses.allStatusesObject
-      const status = statuses[id]
-
-      return status
+    statusId () {
+      return this.$route.params.id
     }
   }
 }
diff --git a/src/components/conversation-page/conversation-page.vue b/src/components/conversation-page/conversation-page.vue
index 532f785c..3db63343 100644
--- a/src/components/conversation-page/conversation-page.vue
+++ b/src/components/conversation-page/conversation-page.vue
@@ -2,7 +2,7 @@
   <conversation
     :collapsable="false"
     is-page="true"
-    :statusoid="statusoid"
+    :statusoid="statusId"
   />
 </template>
 
diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js
index 49fa8612..2be74c1f 100644
--- a/src/components/conversation/conversation.js
+++ b/src/components/conversation/conversation.js
@@ -51,20 +51,20 @@ const conversation = {
   },
   computed: {
     status () {
-      return this.statusoid
+      return this.$store.state.statuses.allStatusesObject[this.statusoid]
     },
     statusId () {
-      if (this.statusoid.retweeted_status) {
-        return this.statusoid.retweeted_status.id
+      if (this.status.retweeted_status) {
+        return this.status.retweeted_status.id
       } else {
-        return this.statusoid.id
+        return this.status.id
       }
     },
     conversationId () {
-      if (this.statusoid.retweeted_status) {
-        return this.statusoid.retweeted_status.statusnet_conversation_id
+      if (this.status.retweeted_status) {
+        return this.status.retweeted_status.statusnet_conversation_id
       } else {
-        return this.statusoid.statusnet_conversation_id
+        return this.status.statusnet_conversation_id
       }
     },
     conversation () {
@@ -127,8 +127,7 @@ const conversation = {
           })
           .then(() => this.setHighlight(this.statusId))
       } else {
-        const id = this.$route.params.id
-        this.$store.state.api.backendInteractor.fetchStatus({ id })
+        this.$store.state.api.backendInteractor.fetchStatus({ id: this.statusoid })
           .then((status) => this.$store.dispatch('addNewStatuses', { statuses: [status] }))
           .then(() => this.fetchConversation())
       }

From 214ab22c4ccc92ddee537204c1c29a1dacb037b8 Mon Sep 17 00:00:00 2001
From: taehoon <th.dev91@gmail.com>
Date: Fri, 30 Aug 2019 11:52:58 -0400
Subject: [PATCH 07/19] update prop binding

---
 src/components/timeline/timeline.vue | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/components/timeline/timeline.vue b/src/components/timeline/timeline.vue
index 4ad51714..14ce21af 100644
--- a/src/components/timeline/timeline.vue
+++ b/src/components/timeline/timeline.vue
@@ -33,7 +33,7 @@
             v-if="timeline.statusesObject[statusId]"
             :key="statusId + '-pinned'"
             class="status-fadein"
-            :statusoid="timeline.statusesObject[statusId]"
+            :statusoid="statusId"
             :collapsable="true"
             :pinned-status-ids-object="pinnedStatusIdsObject"
           />
@@ -43,7 +43,7 @@
             v-if="!excludedStatusIdsObject[status.id]"
             :key="status.id"
             class="status-fadein"
-            :statusoid="status"
+            :statusoid="status.id"
             :collapsable="true"
           />
         </template>

From c1f3b0dc75f657d046b84d358a1679a75261db63 Mon Sep 17 00:00:00 2001
From: taehoon <th.dev91@gmail.com>
Date: Fri, 30 Aug 2019 12:11:59 -0400
Subject: [PATCH 08/19] refactoring

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

diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js
index 2be74c1f..398b7638 100644
--- a/src/components/conversation/conversation.js
+++ b/src/components/conversation/conversation.js
@@ -57,7 +57,7 @@ const conversation = {
       if (this.status.retweeted_status) {
         return this.status.retweeted_status.id
       } else {
-        return this.status.id
+        return this.statusoid
       }
     },
     conversationId () {
@@ -120,23 +120,25 @@ const conversation = {
   methods: {
     fetchConversation () {
       if (this.status) {
-        this.$store.state.api.backendInteractor.fetchConversation({ id: this.status.id })
+        this.$store.state.api.backendInteractor.fetchConversation({ id: this.statusoid })
           .then(({ ancestors, descendants }) => {
             this.$store.dispatch('addNewStatuses', { statuses: ancestors })
             this.$store.dispatch('addNewStatuses', { statuses: descendants })
+            this.setHighlight(this.statusId)
           })
-          .then(() => this.setHighlight(this.statusId))
       } else {
         this.$store.state.api.backendInteractor.fetchStatus({ id: this.statusoid })
-          .then((status) => this.$store.dispatch('addNewStatuses', { statuses: [status] }))
-          .then(() => this.fetchConversation())
+          .then((status) => {
+            this.$store.dispatch('addNewStatuses', { statuses: [status] })
+            this.fetchConversation()
+          })
       }
     },
     getReplies (id) {
       return this.replies[id] || []
     },
     focused (id) {
-      return (this.isExpanded) && id === this.status.id
+      return (this.isExpanded) && id === this.statusoid
     },
     setHighlight (id) {
       if (!id) return

From 482cd52f77fa514ace73a7e17f169c0518f000b1 Mon Sep 17 00:00:00 2001
From: taehoon <th.dev91@gmail.com>
Date: Fri, 30 Aug 2019 12:58:48 -0400
Subject: [PATCH 09/19] stop fetching whole conversation when change
 highlighted status

---
 src/components/conversation/conversation.js | 22 ++++++++++++++-------
 1 file changed, 15 insertions(+), 7 deletions(-)

diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js
index 398b7638..dc58cd58 100644
--- a/src/components/conversation/conversation.js
+++ b/src/components/conversation/conversation.js
@@ -1,4 +1,4 @@
-import { reduce, filter, findIndex, clone } from 'lodash'
+import { reduce, filter, findIndex, clone, get } from 'lodash'
 import Status from '../status/status.vue'
 
 const sortById = (a, b) => {
@@ -61,11 +61,7 @@ const conversation = {
       }
     },
     conversationId () {
-      if (this.status.retweeted_status) {
-        return this.status.retweeted_status.statusnet_conversation_id
-      } else {
-        return this.status.statusnet_conversation_id
-      }
+      return this.getConversationId(this.statusoid)
     },
     conversation () {
       if (!this.status) {
@@ -110,7 +106,15 @@ const conversation = {
     Status
   },
   watch: {
-    status: 'fetchConversation',
+    statusoid (newVal, oldVal) {
+      const newConversationId = this.getConversationId(newVal)
+      const oldConversationId = this.getConversationId(oldVal)
+      if (newConversationId && oldConversationId && newConversationId === oldConversationId) {
+        this.setHighlight(this.statusId)
+      } else {
+        this.fetchConversation()
+      }
+    },
     expanded (value) {
       if (value) {
         this.fetchConversation()
@@ -150,6 +154,10 @@ const conversation = {
     },
     toggleExpanded () {
       this.expanded = !this.expanded
+    },
+    getConversationId (statusId) {
+      const status = this.$store.state.statuses.allStatusesObject[statusId]
+      return get(status, 'retweeted_status.statusnet_conversation_id', get(status, 'statusnet_conversation_id'))
     }
   }
 }

From 9727009147d541bc8ab7083791a531b39f29c614 Mon Sep 17 00:00:00 2001
From: taehoon <th.dev91@gmail.com>
Date: Tue, 3 Sep 2019 13:19:14 -0400
Subject: [PATCH 10/19] update prop name

---
 .../conversation-page/conversation-page.vue   |  2 +-
 src/components/conversation/conversation.js   | 24 +++++++++----------
 src/components/timeline/timeline.vue          |  4 ++--
 3 files changed, 15 insertions(+), 15 deletions(-)

diff --git a/src/components/conversation-page/conversation-page.vue b/src/components/conversation-page/conversation-page.vue
index 3db63343..8cc0a55f 100644
--- a/src/components/conversation-page/conversation-page.vue
+++ b/src/components/conversation-page/conversation-page.vue
@@ -2,7 +2,7 @@
   <conversation
     :collapsable="false"
     is-page="true"
-    :statusoid="statusId"
+    :status-id="statusId"
   />
 </template>
 
diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js
index dc58cd58..10dd8eb0 100644
--- a/src/components/conversation/conversation.js
+++ b/src/components/conversation/conversation.js
@@ -39,7 +39,7 @@ const conversation = {
     }
   },
   props: [
-    'statusoid',
+    'statusId',
     'collapsable',
     'isPage',
     'pinnedStatusIdsObject'
@@ -51,17 +51,17 @@ const conversation = {
   },
   computed: {
     status () {
-      return this.$store.state.statuses.allStatusesObject[this.statusoid]
+      return this.$store.state.statuses.allStatusesObject[this.statusId]
     },
-    statusId () {
+    originalStatusId () {
       if (this.status.retweeted_status) {
         return this.status.retweeted_status.id
       } else {
-        return this.statusoid
+        return this.statusId
       }
     },
     conversationId () {
-      return this.getConversationId(this.statusoid)
+      return this.getConversationId(this.statusId)
     },
     conversation () {
       if (!this.status) {
@@ -73,7 +73,7 @@ const conversation = {
       }
 
       const conversation = clone(this.$store.state.statuses.conversationsObject[this.conversationId])
-      const statusIndex = findIndex(conversation, { id: this.statusId })
+      const statusIndex = findIndex(conversation, { id: this.originalStatusId })
       if (statusIndex !== -1) {
         conversation[statusIndex] = this.status
       }
@@ -106,11 +106,11 @@ const conversation = {
     Status
   },
   watch: {
-    statusoid (newVal, oldVal) {
+    statusId (newVal, oldVal) {
       const newConversationId = this.getConversationId(newVal)
       const oldConversationId = this.getConversationId(oldVal)
       if (newConversationId && oldConversationId && newConversationId === oldConversationId) {
-        this.setHighlight(this.statusId)
+        this.setHighlight(this.originalStatusId)
       } else {
         this.fetchConversation()
       }
@@ -124,14 +124,14 @@ const conversation = {
   methods: {
     fetchConversation () {
       if (this.status) {
-        this.$store.state.api.backendInteractor.fetchConversation({ id: this.statusoid })
+        this.$store.state.api.backendInteractor.fetchConversation({ id: this.statusId })
           .then(({ ancestors, descendants }) => {
             this.$store.dispatch('addNewStatuses', { statuses: ancestors })
             this.$store.dispatch('addNewStatuses', { statuses: descendants })
-            this.setHighlight(this.statusId)
+            this.setHighlight(this.originalStatusId)
           })
       } else {
-        this.$store.state.api.backendInteractor.fetchStatus({ id: this.statusoid })
+        this.$store.state.api.backendInteractor.fetchStatus({ id: this.statusId })
           .then((status) => {
             this.$store.dispatch('addNewStatuses', { statuses: [status] })
             this.fetchConversation()
@@ -142,7 +142,7 @@ const conversation = {
       return this.replies[id] || []
     },
     focused (id) {
-      return (this.isExpanded) && id === this.statusoid
+      return (this.isExpanded) && id === this.statusId
     },
     setHighlight (id) {
       if (!id) return
diff --git a/src/components/timeline/timeline.vue b/src/components/timeline/timeline.vue
index 14ce21af..ba66e6da 100644
--- a/src/components/timeline/timeline.vue
+++ b/src/components/timeline/timeline.vue
@@ -33,7 +33,7 @@
             v-if="timeline.statusesObject[statusId]"
             :key="statusId + '-pinned'"
             class="status-fadein"
-            :statusoid="statusId"
+            :status-id="statusId"
             :collapsable="true"
             :pinned-status-ids-object="pinnedStatusIdsObject"
           />
@@ -43,7 +43,7 @@
             v-if="!excludedStatusIdsObject[status.id]"
             :key="status.id"
             class="status-fadein"
-            :statusoid="status.id"
+            :status-id="status.id"
             :collapsable="true"
           />
         </template>

From ea5b36a597dad2488b2204f6396c2f3d530cdf25 Mon Sep 17 00:00:00 2001
From: taehoon <th.dev91@gmail.com>
Date: Tue, 27 Aug 2019 12:17:10 -0400
Subject: [PATCH 11/19] refactor toggling body visibility using class

---
 src/App.scss                              | 4 ++++
 src/services/style_setter/style_setter.js | 8 ++++----
 2 files changed, 8 insertions(+), 4 deletions(-)

diff --git a/src/App.scss b/src/App.scss
index fac800bc..2e20fb57 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -49,6 +49,10 @@ body {
   overflow-x: hidden;
   -webkit-font-smoothing: antialiased;
   -moz-osx-font-smoothing: grayscale;
+
+  &.hidden {
+    display: none;
+  }
 }
 
 a {
diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js
index f186d202..1cf7edc3 100644
--- a/src/services/style_setter/style_setter.js
+++ b/src/services/style_setter/style_setter.js
@@ -22,7 +22,7 @@ const setStyle = (href, commit) => {
   ***/
   const head = document.head
   const body = document.body
-  body.style.display = 'none'
+  body.classList.add('hidden')
   const cssEl = document.createElement('link')
   cssEl.setAttribute('rel', 'stylesheet')
   cssEl.setAttribute('href', href)
@@ -46,7 +46,7 @@ const setStyle = (href, commit) => {
     head.appendChild(styleEl)
     // const styleSheet = styleEl.sheet
 
-    body.style.display = 'initial'
+    body.classList.remove('hidden')
   }
 
   cssEl.addEventListener('load', setDynamic)
@@ -75,7 +75,7 @@ const applyTheme = (input, commit) => {
   const { rules, theme } = generatePreset(input)
   const head = document.head
   const body = document.body
-  body.style.display = 'none'
+  body.classList.add('hidden')
 
   const styleEl = document.createElement('style')
   head.appendChild(styleEl)
@@ -86,7 +86,7 @@ const applyTheme = (input, commit) => {
   styleSheet.insertRule(`body { ${rules.colors} }`, 'index-max')
   styleSheet.insertRule(`body { ${rules.shadows} }`, 'index-max')
   styleSheet.insertRule(`body { ${rules.fonts} }`, 'index-max')
-  body.style.display = 'initial'
+  body.classList.remove('hidden')
 
   // commit('setOption', { name: 'colors', value: htmlColors })
   // commit('setOption', { name: 'radii', value: radii })

From d0fc509ad11f8af522e9cb071f57f84d386775b9 Mon Sep 17 00:00:00 2001
From: davidyin <bing.h.yin@gmail.com>
Date: Tue, 10 Sep 2019 19:50:06 -0700
Subject: [PATCH 12/19] Complete the Chinese language file based on the version
 0.9.999

---
 src/i18n/zh.json | 443 +++++++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 430 insertions(+), 13 deletions(-)

diff --git a/src/i18n/zh.json b/src/i18n/zh.json
index da6dae5f..1ca70e49 100644
--- a/src/i18n/zh.json
+++ b/src/i18n/zh.json
@@ -2,6 +2,10 @@
   "chat": {
     "title": "聊天"
   },
+  "exporter": {
+    "export": "导出",
+    "processing": "正在处理,稍后会提示您下载文件"
+  },
   "features_panel": {
     "chat": "聊天",
     "gopher": "Gopher",
@@ -17,23 +21,66 @@
   },
   "general": {
     "apply": "应用",
-    "submit": "提交"
+    "submit": "提交",
+    "more": "更多",
+    "generic_error": "发生一个错误",
+    "optional": "可选项",
+    "show_more": "显示更多",
+    "show_less": "显示更少",
+    "cancel": "取消",
+    "disable": "禁用",
+    "enable": "启用",
+    "confirm": "确认",
+    "verify": "验证"
+  },
+  "image_cropper": {
+    "crop_picture": "裁剪图片",
+    "save": "保存",
+    "save_without_cropping": "保存未经裁剪的图片",
+    "cancel": "取消"
+  },
+  "importer": {
+    "submit": "提交",
+    "success": "导入成功。",
+    "error": "导入此文件时出现一个错误。"
   },
   "login": {
     "login": "登录",
+    "description": "用 OAuth 登录",
     "logout": "登出",
     "password": "密码",
     "placeholder": "例如:lain",
     "register": "注册",
-    "username": "用户名"
+    "username": "用户名",
+    "hint": "登录后加入讨论",
+    "authentication_code": "验证码",
+    "enter_recovery_code": "输入一个恢复码",
+    "enter_two_factor_code": "输入一个双重因素验证码",
+    "recovery_code": "恢复码",
+    "heading" : {
+      "totp" : "双重因素验证",
+      "recovery" : "双重因素恢复"
+    }
+  },
+  "media_modal": {
+    "previous": "往前",
+    "next": "往后"
   },
   "nav": {
+    "about": "关于",
+    "back": "Back",
     "chat": "本地聊天",
     "friend_requests": "关注请求",
     "mentions": "提及",
+    "interactions": "互动",
+    "dms": "私信",
     "public_tl": "公共时间线",
     "timeline": "时间线",
-    "twkn": "所有已知网络"
+    "twkn": "所有已知网络",
+    "user_search": "用户搜索",
+    "search": "搜索",
+    "who_to_follow": "推荐关注",
+    "preferences": "偏好设置"
   },
   "notifications": {
     "broken_favorite": "未知的状态,正在搜索中...",
@@ -42,24 +89,57 @@
     "load_older": "加载更早的通知",
     "notifications": "通知",
     "read": "阅读!",
-    "repeated_you": "转发了你的状态"
+    "repeated_you": "转发了你的状态",
+    "no_more_notifications": "没有更多的通知"
+  },
+  "polls": {
+    "add_poll": "增加问卷调查",
+    "add_option": "增加选项",
+    "option": "选项",
+    "votes": "投票",
+    "vote": "投票",
+    "type": "问卷类型",
+    "single_choice": "单选项",
+    "multiple_choices": "多选项",
+    "expiry": "问卷的时间",
+    "expires_in": "投票于 {0} 内结束",
+    "expired": "投票 {0} 前已结束",
+    "not_enough_options": "投票的选项太少"
+  },
+  "stickers": {
+    "add_sticker": "添加贴纸"
+  },
+  "interactions": {
+    "favs_repeats": "转发和收藏",
+    "follows": "新的关注着",
+    "load_older": "加载更早的互动"
   },
   "post_status": {
+    "new_status": "发布新状态",
     "account_not_locked_warning": "你的帐号没有 {0}。任何人都可以关注你并浏览你的上锁内容。",
     "account_not_locked_warning_link": "上锁",
     "attachments_sensitive": "标记附件为敏感内容",
     "content_type": {
-      "text/plain": "纯文本"
+      "text/plain": "纯文本",
+      "text/html": "HTML",
+      "text/markdown": "Markdown",
+      "text/bbcode": "BBCode"
     },
     "content_warning": "主题(可选)",
     "default": "刚刚抵达上海",
-    "direct_warning": "本条内容只有被提及的用户能够看到。",
+    "direct_warning_to_all": "本条内容只有被提及的用户能够看到。",
+    "direct_warning_to_first_only": "本条内容只有被在消息开始处提及的用户能够看到。",
     "posting": "发送",
+    "scope_notice": {
+      "public": "本条内容可以被所有人看到",
+      "private": "关注你的人才能看到本条内容",
+      "unlisted": "本条内容既不在公共时间线,也不会在所有已知网络上可见"
+    },
     "scope": {
       "direct": "私信 - 只发送给被提及的用户",
       "private": "仅关注者 - 只有关注了你的人能看到",
       "public": "公共 - 发送到公共时间轴",
-      "unlisted": "不公开 - 所有人可见,但不会发送到公共时间轴"
+      "unlisted": "不公开 - 不会发送到公共时间轴"
     }
   },
   "registration": {
@@ -68,9 +148,49 @@
     "fullname": "全名",
     "password_confirm": "确认密码",
     "registration": "注册",
-    "token": "邀请码"
+    "token": "邀请码",
+    "captcha": "CAPTCHA",
+    "new_captcha": "点击图片获取新的验证码",
+    "username_placeholder": "例如: lain",
+    "fullname_placeholder": "例如: Lain Iwakura",
+    "bio_placeholder": "例如:\n你好, 我是 Lain.\n我是一个住在上海的宅男。你可能在某处见过我。",
+    "validations": {
+      "username_required": "不能留空",
+      "fullname_required": "不能留空",
+      "email_required": "不能留空",
+      "password_required": "不能留空",
+      "password_confirmation_required": "不能留空",
+      "password_confirmation_match": "密码不一致"
+    }
+  },
+  "selectable_list": {
+    "select_all": "选择全部"
   },
   "settings": {
+    "app_name": "App 名称",
+    "security": "安全",
+    "enter_current_password_to_confirm": "输入你当前密码来确认你的身份",
+    "mfa": {
+      "otp" : "OTP",
+      "setup_otp" : "设置 OTP",
+      "wait_pre_setup_otp" : "预设 OTP",
+      "confirm_and_enable" : "确认并启用 OTP",
+      "title": "双因素验证",
+      "generate_new_recovery_codes" : "生成新的恢复码",
+      "warning_of_generate_new_codes" : "当你生成新的恢复码时,你的就恢复码就失效了。",
+      "recovery_codes" : "恢复码。",
+      "waiting_a_recovery_codes": "接受备份码。。。",
+      "recovery_codes_warning" : "抄写这些号码,或者保存在安全的地方。这些号码不会再次显示。如果你无法访问你的 2FA app,也丢失了你的恢复码,你的账号就再也无法登录了。",
+      "authentication_methods" : "身份验证方法",
+      "scan": {
+        "title": "扫一下",
+        "desc": "使用你的双因素验证 app,扫描这个二维码,或者输入这些文字密钥:",
+        "secret_code": "密钥"
+      },
+      "verify": {
+        "desc": "要启用双因素验证,请把你的双因素验证 app 里的数字输入:"
+      }
+    },
     "attachmentRadius": "附件",
     "attachments": "附件",
     "autoload": "启用滚动到底部时的自动加载",
@@ -79,6 +199,12 @@
     "avatarRadius": "头像",
     "background": "背景",
     "bio": "简介",
+    "block_export": "块导出",
+    "block_export_button": "到处你的块到一个 csv 文件",
+    "block_import": "块导入",
+    "block_import_error": "错误导入块",
+    "blocks_imported": "块导入了!需要一点时间来处理。",
+    "blocks_tab": "块",
     "btnRadius": "按钮",
     "cBlue": "蓝色(回复,关注)",
     "cGreen": "绿色(转发)",
@@ -88,6 +214,7 @@
     "change_password_error": "修改密码的时候出了点问题。",
     "changed_password": "成功修改了密码!",
     "collapse_subject": "折叠带主题的内容",
+    "composing": "正在书写",
     "confirm_new_password": "确认新密码",
     "current_avatar": "当前头像",
     "current_password": "当前密码",
@@ -98,12 +225,12 @@
     "delete_account_description": "永久删除你的帐号和所有消息。",
     "delete_account_error": "删除账户时发生错误,如果一直删除不了,请联系实例管理员。",
     "delete_account_instructions": "在下面输入你的密码来确认删除账户",
+    "avatar_size_instruction": "推荐的头像图片最小的尺寸是 150x150 像素。",
     "export_theme": "导出预置主题",
     "filtering": "过滤器",
     "filtering_explanation": "所有包含以下词汇的内容都会被隐藏,一行一个",
     "follow_export": "导出关注",
     "follow_export_button": "将关注导出成 csv 文件",
-    "follow_export_processing": "正在处理,过一会儿就可以下载你的文件了",
     "follow_import": "导入关注",
     "follow_import_error": "导入关注时错误",
     "follows_imported": "关注已导入!尚需要一些时间来处理。",
@@ -111,12 +238,22 @@
     "general": "通用",
     "hide_attachments_in_convo": "在对话中隐藏附件",
     "hide_attachments_in_tl": "在时间线上隐藏附件",
+    "hide_muted_posts": "不显示被隐藏的用户的帖子",
+    "max_thumbnails": "最多再每个帖子所能显示的缩略图数量",
+    "hide_isp": "隐藏指定实例的面板H",
+    "preload_images": "预载图片",
+    "use_one_click_nsfw": "点击一次以打开工作场所不适宜的附件",
     "hide_post_stats": "隐藏推文相关的统计数据(例如:收藏的次数)",
     "hide_user_stats": "隐藏用户的统计数据(例如:关注者的数量)",
+    "hide_filtered_statuses": "隐藏过滤的状态",
+    "import_blocks_from_a_csv_file": "从 csv 文件中导入块",
     "import_followers_from_a_csv_file": "从 csv 文件中导入关注",
     "import_theme": "导入预置主题",
     "inputRadius": "输入框",
+    "checkboxRadius": "复选框",
     "instance_default": "(默认:{value})",
+    "instance_default_simple": "(默认)",
+    "interface": "界面",
     "interfaceLanguage": "界面语言",
     "invalid_theme_imported": "您所选择的主题文件不被 Pleroma 支持,因此主题未被修改。",
     "limited_availability": "在您的浏览器中无法使用",
@@ -124,6 +261,9 @@
     "lock_account_description": "你需要手动审核关注请求",
     "loop_video": "循环视频",
     "loop_video_silent_only": "只循环没有声音的视频(例如:Mastodon 里的“GIF”)",
+    "mutes_tab": "隐藏",
+    "play_videos_in_modal": "在弹出框内播放视频",
+    "use_contain_fit": "生成缩略图时不要裁剪附件。",
     "name": "名字",
     "name_bio": "名字及简介",
     "new_password": "新密码",
@@ -133,9 +273,15 @@
     "notification_visibility_mentions": "提及",
     "notification_visibility_repeats": "转发",
     "no_rich_text_description": "不显示富文本格式",
+    "no_blocks": "没有块",
+    "no_mutes": "没有隐藏",
+    "hide_follows_description": "不要显示我所关注的人",
+    "hide_followers_description": "不要显示关注我的人",
+    "show_admin_badge": "显示管理徽章",
+    "show_moderator_badge": "显示版主徽章",
     "nsfw_clickthrough": "将不和谐附件隐藏,点击才能打开",
     "oauth_tokens": "OAuth令牌",
-    "token": "代币",
+    "token": "令牌",
     "refresh_token": "刷新令牌",
     "valid_until": "有效期至",
     "revoke_token": "撤消",
@@ -151,25 +297,196 @@
     "reply_visibility_all": "显示所有回复",
     "reply_visibility_following": "只显示发送给我的回复/发送给我关注的用户的回复",
     "reply_visibility_self": "只显示发送给我的回复",
+    "autohide_floating_post_button": "自动隐藏新帖子的按钮(移动设备)",
     "saving_err": "保存设置时发生错误",
     "saving_ok": "设置已保存",
+    "search_user_to_block": "搜索你想屏蔽的用户",
+    "search_user_to_mute": "搜索你想要隐藏的用户",
     "security_tab": "安全",
+    "scope_copy": "回复时的复制范围(私信是总是复制的)",
+    "minimal_scopes_mode": "最小发文范围",
     "set_new_avatar": "设置新头像",
     "set_new_profile_background": "设置新的个人资料背景",
     "set_new_profile_banner": "设置新的横幅图片",
     "settings": "设置",
+    "subject_input_always_show": "总是显示主题框",
+    "subject_line_behavior": "回复时复制主题",
+    "subject_line_email": "比如电邮: \"re: 主题\"",
+    "subject_line_mastodon": "比如 mastodon: copy as is",
+    "subject_line_noop": "不要复制",
+    "post_status_content_type": "发文状态内容类型",
     "stop_gifs": "鼠标悬停时播放GIF",
     "streaming": "开启滚动到顶部时的自动推送",
     "text": "文本",
     "theme": "主题",
     "theme_help": "使用十六进制代码(#rrggbb)来设置主题颜色。",
+    "theme_help_v2_1": "你也可以通过切换复选框来覆盖某些组件的颜色和透明。使用“清除所有”来清楚所有覆盖设置。",
+    "theme_help_v2_2": "某些条目下的图标是背景或文本对比指示器,鼠标悬停可以获取详细信息。请记住,使用透明度来显示最差的情况。",
     "tooltipRadius": "提醒",
+    "upload_a_photo": "上传照片",
     "user_settings": "用户设置",
     "values": {
       "false": "否",
       "true": "是"
+    },
+    "notifications": "通知",
+    "notification_setting": "通知来源:",
+    "notification_setting_follows": "你所关注的用户",
+    "notification_setting_non_follows": "你没有关注的用户",
+    "notification_setting_followers": "关注你的用户",
+    "notification_setting_non_followers": "没有关注你的用户",
+    "notification_mutes": "要停止收到某个指定的用户的通知,请使用隐藏功能。",
+    "notification_blocks": "拉黑一个用户会停掉所有他的通知,等同于取消关注。",
+    "enable_web_push_notifications": "启用 web 推送通知",
+    "style": {
+      "switcher": {
+        "keep_color": "保留颜色",
+        "keep_shadows": "保留阴影",
+        "keep_opacity": "保留透明度",
+        "keep_roundness": "保留圆角",
+        "keep_fonts": "保留字体",
+        "save_load_hint": "\"保留\" 选项在选择或加载主题时保留当前设置的选项,在导出主题时还会存储上述选项。当所有复选框未设置时,导出主题将保存所有内容。",
+        "reset": "重置",
+        "clear_all": "清除全部",
+        "clear_opacity": "清除透明度"
+      },
+      "common": {
+        "color": "颜色",
+        "opacity": "透明度",
+        "contrast": {
+          "hint": "对比度是 {ratio}, 它 {level} {context}",
+          "level": {
+            "aa": "符合 AA 等级准则(最低)",
+            "aaa": "符合 AAA 等级准则(推荐)",
+            "bad": "不符合任何辅助功能指南"
+          },
+          "context": {
+            "18pt": "大字文本 (18pt+)",
+            "text": "文本"
+          }
+        }
+      },
+      "common_colors": {
+        "_tab_label": "常规",
+        "main": "常用颜色",
+        "foreground_hint": "点击”高级“ 标签进行细致的控制",
+        "rgbo": "图标,口音,徽章"
+      },
+      "advanced_colors": {
+        "_tab_label": "高级",
+        "alert": "提醒或警告背景色",
+        "alert_error": "错误",
+        "badge": "徽章背景",
+        "badge_notification": "通知",
+        "panel_header": "面板标题",
+        "top_bar": "顶栏",
+        "borders": "边框",
+        "buttons": "按钮",
+        "inputs": "输入框",
+        "faint_text": "灰度文字"
+      },
+      "radii": {
+        "_tab_label": "圆角"
+      },
+      "shadows": {
+        "_tab_label": "阴影和照明",
+        "component": "组件",
+        "override": "覆盖",
+        "shadow_id": "阴影 #{value}",
+        "blur": "模糊",
+        "spread": "扩散",
+        "inset": "插入内部",
+        "hint": "对于阴影你还可以使用 --variable 作为颜色值来使用 CSS3 变量。请注意,这种情况下,透明设置将不起作用。",
+        "filter_hint": {
+          "always_drop_shadow": "警告,此阴影设置会总是使用 {0} ,如果浏览器支持的话。",
+          "drop_shadow_syntax": "{0} 不支持参数 {1} 和关键词 {2} 。",
+          "avatar_inset": "请注意组合两个内部和非内部的阴影到头像上,在透明头像上可能会有意料之外的效果。",
+          "spread_zero": "阴影的扩散 > 0 会同设置成零一样",
+          "inset_classic": "插入内部的阴影会使用 {0}"
+        },
+        "components": {
+          "panel": "面板",
+          "panelHeader": "面板标题",
+          "topBar": "顶栏",
+          "avatar": "用户头像(在个人资料栏)",
+          "avatarStatus": "用户头像(在帖子显示栏)",
+          "popup": "弹窗和工具提示",
+          "button": "按钮",
+          "buttonHover": "按钮(悬停)",
+          "buttonPressed": "按钮(按下)",
+          "buttonPressedHover": "按钮(按下和悬停)",
+          "input": "输入框"
+        }
+      },
+      "fonts": {
+        "_tab_label": "字体",
+        "help": "给用户界面的元素选择字体。选择 “自选”的你必须输入确切的字体名称。",
+        "components": {
+          "interface": "界面",
+          "input": "输入框",
+          "post": "发帖文字",
+          "postCode": "帖子中使用等间距文字(富文本)"
+        },
+        "family": "字体名称",
+        "size": "大小 (in px)",
+        "weight": "字重 (粗体))",
+        "custom": "自选"
+      },
+      "preview": {
+        "header": "预览",
+        "content": "内容",
+        "error": "例子错误",
+        "button": "按钮",
+        "text": "有堆 {0} 和 {1}",
+        "mono": "内容",
+        "input": "刚刚抵达上海",
+        "faint_link": "帮助菜单",
+        "fine_print": "阅读我们的 {0} 学不到什么东东!",
+        "header_faint": "这很正常",
+        "checkbox": "我已经浏览了 TOC",
+        "link": "一个很棒的摇滚链接"
+      }
+    },
+    "version": {
+      "title": "版本",
+      "backend_version": "后端版本",
+      "frontend_version": "前端版本"
     }
   },
+  "time": {
+    "day": "{0} 天",
+    "days": "{0} 天",
+    "day_short": "{0}d",
+    "days_short": "{0}d",
+    "hour": "{0} 小时",
+    "hours": "{0} 小时",
+    "hour_short": "{0}h",
+    "hours_short": "{0}h",
+    "in_future": "还有 {0}",
+    "in_past": "{0} 之前",
+    "minute": "{0} 分钟",
+    "minutes": "{0} 分钟",
+    "minute_short": "{0}min",
+    "minutes_short": "{0}min",
+    "month": "{0} 月",
+    "months": "{0} 月",
+    "month_short": "{0}mo",
+    "months_short": "{0}mo",
+    "now": "刚刚",
+    "now_short": "刚刚",
+    "second": "{0} 秒",
+    "seconds": "{0} 秒",
+    "second_short": "{0}s",
+    "seconds_short": "{0}s",
+    "week": "{0} 周",
+    "weeks": "{0} 周",
+    "week_short": "{0}w",
+    "weeks_short": "{0}w",
+    "year": "{0} 年",
+    "years": "{0} 年",
+    "year_short": "{0}y",
+    "years_short": "{0}y"
+  },
   "timeline": {
     "collapse": "折叠",
     "conversation": "对话",
@@ -178,29 +495,129 @@
     "no_retweet_hint": "这条内容仅关注者可见,或者是私信,因此不能转发。",
     "repeated": "已转发",
     "show_new": "显示新内容",
-    "up_to_date": "已是最新"
+    "up_to_date": "已是最新",
+    "no_more_statuses": "没有更多的状态",
+    "no_statuses": "没有状态更新"
+  },
+  "status": {
+    "favorites": "收藏",
+    "repeats": "转发",
+    "delete": "删除状态",
+    "pin": "在个人资料置顶",
+    "unpin": "取消在个人资料置顶",
+    "pinned": "置顶",
+    "delete_confirm": "你真的想要删除这条状态吗?",
+    "reply_to": "回复",
+    "replies_list": "回复:",
+    "mute_conversation": "隐藏对话",
+    "unmute_conversation": "对话取消隐藏"
   },
   "user_card": {
     "approve": "允许",
     "block": "屏蔽",
     "blocked": "已屏蔽!",
     "deny": "拒绝",
+    "favorites": "收藏",
     "follow": "关注",
+    "follow_sent": "请求已发送!",
+    "follow_progress": "请求中",
+    "follow_again": "再次发送请求?",
+    "follow_unfollow": "取消关注",
     "followees": "正在关注",
     "followers": "关注者",
     "following": "正在关注!",
     "follows_you": "关注了你!",
+    "its_you": "就是你!!",
+    "media": "媒体",
     "mute": "隐藏",
     "muted": "已隐藏",
     "per_day": "每天",
     "remote_follow": "跨站关注",
-    "statuses": "状态"
+    "report": "报告", 
+    "statuses": "状态",
+    "subscribe": "订阅",
+    "unsubscribe": "退订",
+    "unblock": "取消拉黑",
+    "unblock_progress": "取消拉黑中...",
+    "block_progress": "拉黑中...",
+    "unmute": "取消隐藏",
+    "unmute_progress": "取消隐藏中...",
+    "mute_progress": "隐藏中...",
+    "admin_menu": {
+      "moderation": "权限",
+      "grant_admin": "赋予管理权限",
+      "revoke_admin": "撤销管理权限",
+      "grant_moderator": "赋予版主权限",
+      "revoke_moderator": "撤销版主权限",
+      "activate_account": "激活账号",
+      "deactivate_account": "关闭账号",
+      "delete_account": "删除账号",
+      "force_nsfw": "标记所有的帖子都是 - 工作场合不适",
+      "strip_media": "从帖子里删除媒体文件",
+      "force_unlisted": "强制帖子为不公开",
+      "sandbox": "强制帖子为只有关注者可看",
+      "disable_remote_subscription": "禁止从远程实例关注用户",
+      "disable_any_subscription": "完全禁止关注用户",
+      "quarantine": "从联合实例中禁止用户帖子",
+      "delete_user": "删除用户",
+      "delete_user_confirmation": "你确认吗?此操作无法撤销。"
+    }
   },
   "user_profile": {
-    "timeline_title": "用户时间线"
+    "timeline_title": "用户时间线",
+    "profile_does_not_exist": "抱歉,此个人资料不存在。",
+    "profile_loading_error": "抱歉,载入个人资料时出错。"
+  },
+  "user_reporting": {
+    "title": "报告 {0}",
+    "add_comment_description": "此报告会发送给你的实例管理员。你可以在下面提供更多详细信息解释报告的缘由:",
+    "additional_comments": "其它信息",
+    "forward_description": "这个账号是从另外一个服务器。同时发送一个副本到那里?",
+    "forward_to": "转发 {0}",
+    "submit": "提交",
+    "generic_error": "当处理你的请求时,发生了一个错误。"
   },
   "who_to_follow": {
     "more": "更多",
     "who_to_follow": "推荐关注"
+  },
+  "tool_tip": {
+    "media_upload": "上传多媒体",
+    "repeat": "转发",
+    "reply": "回复",
+    "favorite": "收藏",
+    "user_settings": "用户设置"
+  },
+  "upload":{
+    "error": {
+      "base": "上传不成功。",
+      "file_too_big": "文件太大了 [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]",
+      "default": "迟些再试"
+    },
+    "file_size_units": {
+      "B": "B",
+      "KiB": "KiB",
+      "MiB": "MiB",
+      "GiB": "GiB",
+      "TiB": "TiB"
+    }
+  },
+  "search": {
+    "people": "人",
+    "hashtags": "Hashtags",
+    "person_talking": "{count} 人谈论",
+    "people_talking": "{count} 人谈论",
+    "no_results": "没有搜索结果"
+  },
+  "password_reset": {
+    "forgot_password": "忘记密码了?",
+    "password_reset": "重置密码",
+    "instruction": "输入你的电邮地址或者用户名,我们将发送一个链接到你的邮箱,用于重置密码。",
+    "placeholder": "你的电邮地址或者用户名",
+    "check_email": "检查你的邮箱,会有一个链接用于重置密码。",
+    "return_home": "回到首页",
+    "not_found": "我们无法找到匹配的邮箱地址或者用户名。",
+    "too_many_requests": "你触发了尝试的限制,请稍后再试。",
+    "password_reset_disabled": "密码重置已经被禁用。请联系你的实例管理员。"
   }
 }

From a8cddecd41b4b6541b65ba78418beaa0682a398b Mon Sep 17 00:00:00 2001
From: davidyin <bing.h.yin@gmail.com>
Date: Wed, 11 Sep 2019 22:37:19 -0700
Subject: [PATCH 13/19] fix some translation about blocks

---
 src/i18n/zh.json | 14 +++++++-------
 1 file changed, 7 insertions(+), 7 deletions(-)

diff --git a/src/i18n/zh.json b/src/i18n/zh.json
index 1ca70e49..80c4e0d8 100644
--- a/src/i18n/zh.json
+++ b/src/i18n/zh.json
@@ -199,11 +199,11 @@
     "avatarRadius": "头像",
     "background": "背景",
     "bio": "简介",
-    "block_export": "块导出",
-    "block_export_button": "到处你的块到一个 csv 文件",
-    "block_import": "块导入",
-    "block_import_error": "错误导入块",
-    "blocks_imported": "块导入了!需要一点时间来处理。",
+    "block_export": "拉黑名单导出",
+    "block_export_button": "导出你的拉黑名单到一个 csv 文件",
+    "block_import": "拉黑名单导入",
+    "block_import_error": "导入拉黑名单出错",
+    "blocks_imported": "拉黑名单导入成功!需要一点时间来处理。",
     "blocks_tab": "块",
     "btnRadius": "按钮",
     "cBlue": "蓝色(回复,关注)",
@@ -246,7 +246,7 @@
     "hide_post_stats": "隐藏推文相关的统计数据(例如:收藏的次数)",
     "hide_user_stats": "隐藏用户的统计数据(例如:关注者的数量)",
     "hide_filtered_statuses": "隐藏过滤的状态",
-    "import_blocks_from_a_csv_file": "从 csv 文件中导入块",
+    "import_blocks_from_a_csv_file": "从 csv 文件中导入拉黑名单",
     "import_followers_from_a_csv_file": "从 csv 文件中导入关注",
     "import_theme": "导入预置主题",
     "inputRadius": "输入框",
@@ -273,7 +273,7 @@
     "notification_visibility_mentions": "提及",
     "notification_visibility_repeats": "转发",
     "no_rich_text_description": "不显示富文本格式",
-    "no_blocks": "没有块",
+    "no_blocks": "没有拉黑的",
     "no_mutes": "没有隐藏",
     "hide_follows_description": "不要显示我所关注的人",
     "hide_followers_description": "不要显示关注我的人",

From 794481d76c6e5e73ca2a575485daa6f890d2ff61 Mon Sep 17 00:00:00 2001
From: Shpuld Shpuldson <shpuld@shpposter.club>
Date: Fri, 13 Sep 2019 20:16:05 +0300
Subject: [PATCH 14/19] fix italics not being selectable

---
 src/App.scss | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/App.scss b/src/App.scss
index 2e20fb57..ea7b54e8 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -16,7 +16,7 @@
   background-position: 0 50%;
 }
 
-i {
+i[class^='icon-'] {
   user-select: none;
 }
 

From 09deb69bc78fb5cfd3d9ff2455a5632ed71dd14b Mon Sep 17 00:00:00 2001
From: taehoon <th.dev91@gmail.com>
Date: Fri, 13 Sep 2019 15:34:17 -0400
Subject: [PATCH 15/19] do not collapse muted user's posts on muted user's
 profile page

---
 src/components/conversation/conversation.js  | 3 ++-
 src/components/conversation/conversation.vue | 1 +
 src/components/status/status.js              | 5 +++--
 src/components/timeline/timeline.js          | 3 ++-
 src/components/timeline/timeline.vue         | 2 ++
 src/components/user_profile/user_profile.vue | 1 +
 6 files changed, 11 insertions(+), 4 deletions(-)

diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js
index 10dd8eb0..44dc49bc 100644
--- a/src/components/conversation/conversation.js
+++ b/src/components/conversation/conversation.js
@@ -42,7 +42,8 @@ const conversation = {
     'statusId',
     'collapsable',
     'isPage',
-    'pinnedStatusIdsObject'
+    'pinnedStatusIdsObject',
+    'forceUnmute'
   ],
   created () {
     if (this.isPage) {
diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue
index f184c071..ba138189 100644
--- a/src/components/conversation/conversation.vue
+++ b/src/components/conversation/conversation.vue
@@ -26,6 +26,7 @@
       :in-conversation="isExpanded"
       :highlight="getHighlight()"
       :replies="getReplies(status.id)"
+      :force-unmute="forceUnmute"
       class="status-fadein panel-body"
       @goto="setHighlight"
       @toggleExpanded="toggleExpanded"
diff --git a/src/components/status/status.js b/src/components/status/status.js
index 502d9583..474a0480 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -29,7 +29,8 @@ const Status = {
     'isPreview',
     'noHeading',
     'inlineExpanded',
-    'showPinned'
+    'showPinned',
+    'forceUnmute'
   ],
   data () {
     return {
@@ -117,7 +118,7 @@ const Status = {
 
       return hits
     },
-    muted () { return !this.unmuted && (this.status.user.muted || this.muteWordHits.length > 0) },
+    muted () { return !this.forceUnmute && !this.unmuted && (this.status.user.muted || this.muteWordHits.length > 0) },
     hideFilteredStatuses () {
       return typeof this.$store.state.config.hideFilteredStatuses === 'undefined'
         ? this.$store.state.instance.hideFilteredStatuses
diff --git a/src/components/timeline/timeline.js b/src/components/timeline/timeline.js
index 8df48f7f..3d5f9de8 100644
--- a/src/components/timeline/timeline.js
+++ b/src/components/timeline/timeline.js
@@ -25,7 +25,8 @@ const Timeline = {
     'tag',
     'embedded',
     'count',
-    'pinnedStatusIds'
+    'pinnedStatusIds',
+    'forceUnmute'
   ],
   data () {
     return {
diff --git a/src/components/timeline/timeline.vue b/src/components/timeline/timeline.vue
index ba66e6da..b89f505a 100644
--- a/src/components/timeline/timeline.vue
+++ b/src/components/timeline/timeline.vue
@@ -36,6 +36,7 @@
             :status-id="statusId"
             :collapsable="true"
             :pinned-status-ids-object="pinnedStatusIdsObject"
+            :force-unmute="forceUnmute"
           />
         </template>
         <template v-for="status in timeline.visibleStatuses">
@@ -45,6 +46,7 @@
             class="status-fadein"
             :status-id="status.id"
             :collapsable="true"
+            :force-unmute="forceUnmute"
           />
         </template>
       </div>
diff --git a/src/components/user_profile/user_profile.vue b/src/components/user_profile/user_profile.vue
index 42516916..c380d2dc 100644
--- a/src/components/user_profile/user_profile.vue
+++ b/src/components/user_profile/user_profile.vue
@@ -26,6 +26,7 @@
           timeline-name="user"
           :user-id="userId"
           :pinned-status-ids="user.pinnedStatusIds"
+          :force-unmute="true"
         />
         <div
           v-if="followsTabVisible"

From b10558f721e68bb57595ed35bec433aec46bf098 Mon Sep 17 00:00:00 2001
From: taehoon <th.dev91@gmail.com>
Date: Fri, 13 Sep 2019 16:17:51 -0400
Subject: [PATCH 16/19] do not collapse muted user's posts on muted user's
 media timeline

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

diff --git a/src/components/user_profile/user_profile.vue b/src/components/user_profile/user_profile.vue
index c380d2dc..f463eea5 100644
--- a/src/components/user_profile/user_profile.vue
+++ b/src/components/user_profile/user_profile.vue
@@ -70,6 +70,7 @@
           timeline-name="media"
           :timeline="media"
           :user-id="userId"
+          :force-unmute="true"
         />
         <Timeline
           v-if="isUs"

From 2f79a7b4a0e54092eff8a0af385ce6fe4c300cb7 Mon Sep 17 00:00:00 2001
From: taehoon <th.dev91@gmail.com>
Date: Fri, 13 Sep 2019 16:55:17 -0400
Subject: [PATCH 17/19] do not change word based muting logic

---
 src/components/conversation/conversation.js  | 2 +-
 src/components/conversation/conversation.vue | 2 +-
 src/components/status/status.js              | 4 ++--
 src/components/timeline/timeline.js          | 2 +-
 src/components/timeline/timeline.vue         | 4 ++--
 src/components/user_profile/user_profile.vue | 5 +++--
 6 files changed, 10 insertions(+), 9 deletions(-)

diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js
index 44dc49bc..72ee9c39 100644
--- a/src/components/conversation/conversation.js
+++ b/src/components/conversation/conversation.js
@@ -43,7 +43,7 @@ const conversation = {
     'collapsable',
     'isPage',
     'pinnedStatusIdsObject',
-    'forceUnmute'
+    'inProfile'
   ],
   created () {
     if (this.isPage) {
diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue
index ba138189..0f1de55f 100644
--- a/src/components/conversation/conversation.vue
+++ b/src/components/conversation/conversation.vue
@@ -26,7 +26,7 @@
       :in-conversation="isExpanded"
       :highlight="getHighlight()"
       :replies="getReplies(status.id)"
-      :force-unmute="forceUnmute"
+      :in-profile="inProfile"
       class="status-fadein panel-body"
       @goto="setHighlight"
       @toggleExpanded="toggleExpanded"
diff --git a/src/components/status/status.js b/src/components/status/status.js
index 474a0480..e8966f47 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -30,7 +30,7 @@ const Status = {
     'noHeading',
     'inlineExpanded',
     'showPinned',
-    'forceUnmute'
+    'inProfile'
   ],
   data () {
     return {
@@ -118,7 +118,7 @@ const Status = {
 
       return hits
     },
-    muted () { return !this.forceUnmute && !this.unmuted && (this.status.user.muted || this.muteWordHits.length > 0) },
+    muted () { return !this.unmuted && ((!this.inProfile && this.status.user.muted) || this.muteWordHits.length > 0) },
     hideFilteredStatuses () {
       return typeof this.$store.state.config.hideFilteredStatuses === 'undefined'
         ? this.$store.state.instance.hideFilteredStatuses
diff --git a/src/components/timeline/timeline.js b/src/components/timeline/timeline.js
index 3d5f9de8..0594576c 100644
--- a/src/components/timeline/timeline.js
+++ b/src/components/timeline/timeline.js
@@ -26,7 +26,7 @@ const Timeline = {
     'embedded',
     'count',
     'pinnedStatusIds',
-    'forceUnmute'
+    'inProfile'
   ],
   data () {
     return {
diff --git a/src/components/timeline/timeline.vue b/src/components/timeline/timeline.vue
index b89f505a..f1d3903a 100644
--- a/src/components/timeline/timeline.vue
+++ b/src/components/timeline/timeline.vue
@@ -36,7 +36,7 @@
             :status-id="statusId"
             :collapsable="true"
             :pinned-status-ids-object="pinnedStatusIdsObject"
-            :force-unmute="forceUnmute"
+            :in-profile="inProfile"
           />
         </template>
         <template v-for="status in timeline.visibleStatuses">
@@ -46,7 +46,7 @@
             class="status-fadein"
             :status-id="status.id"
             :collapsable="true"
-            :force-unmute="forceUnmute"
+            :in-profile="inProfile"
           />
         </template>
       </div>
diff --git a/src/components/user_profile/user_profile.vue b/src/components/user_profile/user_profile.vue
index f463eea5..14082e83 100644
--- a/src/components/user_profile/user_profile.vue
+++ b/src/components/user_profile/user_profile.vue
@@ -26,7 +26,7 @@
           timeline-name="user"
           :user-id="userId"
           :pinned-status-ids="user.pinnedStatusIds"
-          :force-unmute="true"
+          :in-profile="true"
         />
         <div
           v-if="followsTabVisible"
@@ -70,7 +70,7 @@
           timeline-name="media"
           :timeline="media"
           :user-id="userId"
-          :force-unmute="true"
+          :in-profile="true"
         />
         <Timeline
           v-if="isUs"
@@ -81,6 +81,7 @@
           :title="$t('user_card.favorites')"
           timeline-name="favorites"
           :timeline="favorites"
+          :in-profile="true"
         />
       </tab-switcher>
     </div>

From 6a3f2832565392526c648c070a2e1275c37f83ea Mon Sep 17 00:00:00 2001
From: taehoon <th.dev91@gmail.com>
Date: Fri, 13 Sep 2019 22:59:24 -0400
Subject: [PATCH 18/19] detect thread-muted posts

---
 src/components/status/status.js                             | 2 +-
 src/services/entity_normalizer/entity_normalizer.service.js | 1 +
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/components/status/status.js b/src/components/status/status.js
index e8966f47..1c38b0f1 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -118,7 +118,7 @@ const Status = {
 
       return hits
     },
-    muted () { return !this.unmuted && ((!this.inProfile && this.status.user.muted) || this.muteWordHits.length > 0) },
+    muted () { return !this.unmuted && ((!this.inProfile && this.status.user.muted) || this.status.thread_muted || this.muteWordHits.length > 0) },
     hideFilteredStatuses () {
       return typeof this.$store.state.config.hideFilteredStatuses === 'undefined'
         ? this.$store.state.instance.hideFilteredStatuses
diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
index 6cc1851d..7438cd90 100644
--- a/src/services/entity_normalizer/entity_normalizer.service.js
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -224,6 +224,7 @@ export const parseStatus = (data) => {
       output.statusnet_conversation_id = data.pleroma.conversation_id
       output.is_local = pleroma.local
       output.in_reply_to_screen_name = data.pleroma.in_reply_to_account_acct
+      output.thread_muted = pleroma.thread_muted
     } else {
       output.text = data.content
       output.summary = data.spoiler_text

From baebf08d20eed14d06f9253e4cc939891955f8cb Mon Sep 17 00:00:00 2001
From: taehoon <th.dev91@gmail.com>
Date: Fri, 13 Sep 2019 22:59:45 -0400
Subject: [PATCH 19/19] do not collapse thread muted posts in conversation

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

diff --git a/src/components/status/status.js b/src/components/status/status.js
index 1c38b0f1..d17ba318 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -118,7 +118,7 @@ const Status = {
 
       return hits
     },
-    muted () { return !this.unmuted && ((!this.inProfile && this.status.user.muted) || this.status.thread_muted || this.muteWordHits.length > 0) },
+    muted () { return !this.unmuted && ((!this.inProfile && this.status.user.muted) || (!this.inConversation && this.status.thread_muted) || this.muteWordHits.length > 0) },
     hideFilteredStatuses () {
       return typeof this.$store.state.config.hideFilteredStatuses === 'undefined'
         ? this.$store.state.instance.hideFilteredStatuses