From 13725f040bca346a7b35b832f36f4e86c5da11e4 Mon Sep 17 00:00:00 2001
From: taehoon <th.dev91@gmail.com>
Date: Thu, 7 Feb 2019 03:05:59 -0500
Subject: Add avatar crop popup

---
 src/components/user_settings/user_settings.vue | 52 +++++++++++++++++++++-----
 1 file changed, 42 insertions(+), 10 deletions(-)

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

diff --git a/src/components/user_settings/user_settings.vue b/src/components/user_settings/user_settings.vue
index d2381da2..9fcd3752 100644
--- a/src/components/user_settings/user_settings.vue
+++ b/src/components/user_settings/user_settings.vue
@@ -47,20 +47,20 @@
           <div class="setting-item">
             <h2>{{$t('settings.avatar')}}</h2>
             <p class="visibility-notice">{{$t('settings.avatar_size_instruction')}}</p>
-            <p>{{$t('settings.current_avatar')}}</p>
-            <img :src="user.profile_image_url_original" class="old-avatar"></img>
-            <p>{{$t('settings.set_new_avatar')}}</p>
-            <img class="new-avatar" v-bind:src="avatarPreview" v-if="avatarPreview">
-            </img>
-            <div>
-              <input type="file" @change="uploadFile('avatar', $event)" ></input>
+            <div class="avatar-upload-wrapper">
+              <div class="avatar-upload">
+                <img :src="user.profile_image_url_original" class="avatar" />
+                <div class="avatar-upload-loading-wrapper" v-if="avatarUploading">
+                  <i class="icon-spin4 animate-spin"></i>
+                </div>
+              </div>
             </div>
-            <i class="icon-spin4 animate-spin" v-if="avatarUploading"></i>
-            <button class="btn btn-default" v-else-if="avatarPreview" @click="submitAvatar">{{$t('general.submit')}}</button>
-            <div class='alert error' v-if="avatarUploadError">
+            <button class="btn" type="button" id="pick-avatar" :disabled="avatarUploading">{{$t('settings.set_new_avatar')}}</button>
+            <div class="alert error" v-if="avatarUploadError">
               Error: {{ avatarUploadError }}
               <i class="button-icon icon-cancel" @click="clearUploadError('avatar')"></i>
             </div>
+            <image-cropper trigger="#pick-avatar" :title="$t('settings.crop_your_new_avatar')" :saveButtonLabel="$t('settings.set_new_avatar')" @submit="submitAvatar" />
           </div>
           <div class="setting-item">
             <h2>{{$t('settings.profile_banner')}}</h2>
@@ -167,6 +167,8 @@
 </script>
 
 <style lang="scss">
+@import '../../_variables.scss';
+
 .profile-edit {
   .bio {
     margin: 0;
@@ -193,5 +195,35 @@
   .bg {
     max-width: 100%;
   }
+
+  .avatar-upload {
+    display: inline-block;
+    position: relative;
+  }
+
+  .avatar-upload-loading-wrapper {
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    background: rgba(0,0,0,.3);
+
+    i {
+      font-size: 50px;
+      color: #FFF;
+    }
+  }
+
+  .avatar {
+    display: block;
+    width: 150px;
+    height: 150px;
+    border-radius: $fallback--avatarRadius;
+    border-radius: var(--avatarRadius, $fallback--avatarRadius);
+  }
 }
 </style>
-- 
cgit v1.2.3-70-g09d2


From b24db12e1cc318e862169355a22f051d3ea2e809 Mon Sep 17 00:00:00 2001
From: taehoon <th.dev91@gmail.com>
Date: Fri, 8 Feb 2019 21:59:33 -0500
Subject: Make embedded image cropper

---
 src/components/image_cropper/image_cropper.js  | 37 ++++++++++++++--------
 src/components/image_cropper/image_cropper.vue | 25 ++++++++-------
 src/components/user_settings/user_settings.js  |  9 ++----
 src/components/user_settings/user_settings.vue | 43 ++++----------------------
 src/i18n/en.json                               |  5 +--
 5 files changed, 50 insertions(+), 69 deletions(-)

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

diff --git a/src/components/image_cropper/image_cropper.js b/src/components/image_cropper/image_cropper.js
index b2580240..4eaa08d4 100644
--- a/src/components/image_cropper/image_cropper.js
+++ b/src/components/image_cropper/image_cropper.js
@@ -1,5 +1,4 @@
 import Cropper from 'cropperjs'
-import Modal from '../modal/modal.vue'
 import 'cropperjs/dist/cropper.css'
 
 const ImageCropper = {
@@ -8,6 +7,10 @@ const ImageCropper = {
       type: [String, window.Element],
       required: true
     },
+    submitHandler: {
+      type: Function,
+      required: true
+    },
     cropperOptions: {
       type: Object,
       default () {
@@ -25,10 +28,10 @@ const ImageCropper = {
       type: String,
       default: 'image/png, image/gif, image/jpeg, image/bmp, image/x-icon'
     },
-    title: {
+    saveButtonLabel: {
       type: String
     },
-    saveButtonLabel: {
+    cancelButtonLabel: {
       type: String
     }
   },
@@ -36,18 +39,17 @@ const ImageCropper = {
     return {
       cropper: undefined,
       dataUrl: undefined,
-      filename: undefined
+      filename: undefined,
+      submitting: false,
+      submitError: null
     }
   },
-  components: {
-    Modal
-  },
   computed: {
-    modalTitle () {
-      return this.title || this.$t('image_cropper.crop_picture')
-    },
-    modalSaveButtonLabel () {
+    saveText () {
       return this.saveButtonLabel || this.$t('image_cropper.save')
+    },
+    cancelText () {
+      return this.cancelButtonLabel || this.$t('image_cropper.cancel')
     }
   },
   methods: {
@@ -57,10 +59,15 @@ const ImageCropper = {
       }
       this.$refs.input.value = ''
       this.dataUrl = undefined
+      this.$emit('close')
     },
     submit () {
-      this.$emit('submit', this.cropper, this.filename)
-      this.destroy()
+      this.submitting = true
+      this.avatarUploadError = null
+      this.submitHandler(this.cropper, this.filename)
+        .then(() => this.destroy())
+        .catch(err => this.submitError = err)
+        .finally(() => this.submitting = false)
     },
     pickImage () {
       this.$refs.input.click()
@@ -77,11 +84,15 @@ const ImageCropper = {
         let reader = new window.FileReader()
         reader.onload = (e) => {
           this.dataUrl = e.target.result
+          this.$emit('open')
         }
         reader.readAsDataURL(fileInput.files[0])
         this.filename = fileInput.files[0].name || 'unknown'
         this.$emit('changed', fileInput.files[0], reader)
       }
+    },
+    clearError () {
+      this.submitError = null
     }
   },
   mounted () {
diff --git a/src/components/image_cropper/image_cropper.vue b/src/components/image_cropper/image_cropper.vue
index b2367128..aa895863 100644
--- a/src/components/image_cropper/image_cropper.vue
+++ b/src/components/image_cropper/image_cropper.vue
@@ -1,15 +1,19 @@
 <template>
   <div class="image-cropper">
-    <modal :show="dataUrl" :title="modalTitle" @close="destroy">
-      <div class="modal-body">
-        <div class="image-cropper-image-container">
-          <img ref="img" :src="dataUrl" alt="" @load.stop="createCropper" />
-        </div>
+    <div v-if="dataUrl">
+      <div class="image-cropper-image-container">
+        <img ref="img" :src="dataUrl" alt="" @load.stop="createCropper" />
       </div>
-      <div class="modal-footer">
-        <button class="btn image-cropper-btn" type="button" @click="submit" v-text="modalSaveButtonLabel"></button>
+      <div class="image-cropper-buttons-wrapper">
+        <button class="btn" type="button" :disabled="submitting" @click="submit" v-text="saveText"></button>
+        <button class="btn" type="button" :disabled="submitting" @click="destroy" v-text="cancelText"></button>
+        <i class="icon-spin4 animate-spin" v-if="submitting"></i>
       </div>
-    </modal>
+      <div class="alert error" v-if="submitError">
+        Error: {{ submitError }}
+        <i class="button-icon icon-cancel" @click="clearError"></i>
+      </div>
+    </div>
     <input ref="input" type="file" class="image-cropper-img-input" :accept="mimes">
   </div>
 </template>
@@ -31,9 +35,8 @@
     }
   }
 
-  &-btn {
-    display: block;
-    width: 100%;
+  &-buttons-wrapper {
+    margin-top: 15px;
   }
 }
 </style>
diff --git a/src/components/user_settings/user_settings.js b/src/components/user_settings/user_settings.js
index 8987c691..2d521c14 100644
--- a/src/components/user_settings/user_settings.js
+++ b/src/components/user_settings/user_settings.js
@@ -21,13 +21,12 @@ const UserSettings = {
       followImportError: false,
       followsImported: false,
       enableFollowsExport: true,
-      avatarUploading: false,
+      pickAvatarBtnVisible: true,
       bannerUploading: false,
       backgroundUploading: false,
       followListUploading: false,
       bannerPreview: null,
       backgroundPreview: null,
-      avatarUploadError: null,
       bannerUploadError: null,
       backgroundUploadError: null,
       deletingAccount: false,
@@ -120,15 +119,13 @@ const UserSettings = {
     },
     submitAvatar (cropper) {
       const img = cropper.getCroppedCanvas({ minWidth: 150, minHeight: 150 }).toDataURL('image/jpeg')
-      this.avatarUploading = true
-      this.$store.state.api.backendInteractor.updateAvatar({ params: { img } }).then((user) => {
+      return this.$store.state.api.backendInteractor.updateAvatar({ params: { img } }).then((user) => {
         if (!user.error) {
           this.$store.commit('addNewUsers', [user])
           this.$store.commit('setCurrentUser', user)
         } else {
-          this.avatarUploadError = this.$t('upload.error.base') + user.error
+          throw this.$t('upload.error.base') + user.error
         }
-        this.avatarUploading = false
       })
     },
     clearUploadError (slot) {
diff --git a/src/components/user_settings/user_settings.vue b/src/components/user_settings/user_settings.vue
index 9fcd3752..8ab92e95 100644
--- a/src/components/user_settings/user_settings.vue
+++ b/src/components/user_settings/user_settings.vue
@@ -47,20 +47,11 @@
           <div class="setting-item">
             <h2>{{$t('settings.avatar')}}</h2>
             <p class="visibility-notice">{{$t('settings.avatar_size_instruction')}}</p>
-            <div class="avatar-upload-wrapper">
-              <div class="avatar-upload">
-                <img :src="user.profile_image_url_original" class="avatar" />
-                <div class="avatar-upload-loading-wrapper" v-if="avatarUploading">
-                  <i class="icon-spin4 animate-spin"></i>
-                </div>
-              </div>
-            </div>
-            <button class="btn" type="button" id="pick-avatar" :disabled="avatarUploading">{{$t('settings.set_new_avatar')}}</button>
-            <div class="alert error" v-if="avatarUploadError">
-              Error: {{ avatarUploadError }}
-              <i class="button-icon icon-cancel" @click="clearUploadError('avatar')"></i>
-            </div>
-            <image-cropper trigger="#pick-avatar" :title="$t('settings.crop_your_new_avatar')" :saveButtonLabel="$t('settings.set_new_avatar')" @submit="submitAvatar" />
+            <p>{{$t('settings.current_avatar')}}</p>
+            <img :src="user.profile_image_url_original" class="current-avatar"></img>
+            <p>{{$t('settings.set_new_avatar')}}</p>
+            <button class="btn" type="button" id="pick-avatar" v-show="pickAvatarBtnVisible">{{$t('settings.upload_a_photo')}}</button>
+            <image-cropper trigger="#pick-avatar" :submitHandler="submitAvatar" @open="pickAvatarBtnVisible=false" @close="pickAvatarBtnVisible=true" />
           </div>
           <div class="setting-item">
             <h2>{{$t('settings.profile_banner')}}</h2>
@@ -196,29 +187,7 @@
     max-width: 100%;
   }
 
-  .avatar-upload {
-    display: inline-block;
-    position: relative;
-  }
-
-  .avatar-upload-loading-wrapper {
-    position: absolute;
-    top: 0;
-    left: 0;
-    right: 0;
-    bottom: 0;
-    display: flex;
-    justify-content: center;
-    align-items: center;
-    background: rgba(0,0,0,.3);
-
-    i {
-      font-size: 50px;
-      color: #FFF;
-    }
-  }
-
-  .avatar {
+  .current-avatar {
     display: block;
     width: 150px;
     height: 150px;
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 3e9ac157..af62acfc 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -23,7 +23,8 @@
   },
   "image_cropper": {
     "crop_picture": "Crop picture",
-    "save": "Save"
+    "save": "Save",
+    "cancel": "Cancel"
   },
   "login": {
     "login": "Log in",
@@ -116,7 +117,6 @@
     "collapse_subject": "Collapse posts with subjects",
     "composing": "Composing",
     "confirm_new_password": "Confirm new password",
-    "crop_your_new_avatar": "Crop your new avatar",
     "current_avatar": "Your current avatar",
     "current_password": "Current password",
     "current_profile_banner": "Your current profile banner",
@@ -211,6 +211,7 @@
     "theme_help_v2_1": "You can also override certain component's colors and opacity by toggling the checkbox, use \"Clear all\" button to clear all overrides.",
     "theme_help_v2_2": "Icons underneath some entries are background/text contrast indicators, hover over for detailed info. Please keep in mind that when using transparency contrast indicators show the worst possible case.",
     "tooltipRadius": "Tooltips/alerts",
+    "upload_a_photo": "Upload a photo",
     "user_settings": "User Settings",
     "values": {
       "false": "no",
-- 
cgit v1.2.3-70-g09d2


From 63cfe051c7f5419da3942632f3a5453f6cd5c769 Mon Sep 17 00:00:00 2001
From: dave <starpumadev@gmail.com>
Date: Tue, 19 Feb 2019 12:38:49 -0500
Subject: #371: show notification when user setting's saved

---
 src/components/user_settings/user_settings.js  |  3 +++
 src/components/user_settings/user_settings.vue | 15 ++++++++++++++-
 src/lib/persisted_state.js                     |  4 ++--
 3 files changed, 19 insertions(+), 3 deletions(-)

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

diff --git a/src/components/user_settings/user_settings.js b/src/components/user_settings/user_settings.js
index d20bf308..3ad711c2 100644
--- a/src/components/user_settings/user_settings.js
+++ b/src/components/user_settings/user_settings.js
@@ -60,6 +60,9 @@ const UserSettings = {
         private: { selected: this.newDefaultScope === 'private' },
         direct: { selected: this.newDefaultScope === 'direct' }
       }
+    },
+    currentSaveStateNotice () {
+      return this.$store.state.interface.settings.currentSaveStateNotice
     }
   },
   methods: {
diff --git a/src/components/user_settings/user_settings.vue b/src/components/user_settings/user_settings.vue
index d2381da2..d1770562 100644
--- a/src/components/user_settings/user_settings.vue
+++ b/src/components/user_settings/user_settings.vue
@@ -1,7 +1,20 @@
 <template>
   <div class="settings panel panel-default">
     <div class="panel-heading">
-      {{$t('settings.user_settings')}}
+      <div class="title">
+        {{$t('settings.user_settings')}}
+      </div>
+      <transition name="fade">
+        <template v-if="currentSaveStateNotice">
+          <div @click.prevent class="alert error" v-if="currentSaveStateNotice.error">
+            {{ $t('settings.saving_err') }}
+          </div>
+
+          <div @click.prevent class="alert transparent" v-if="!currentSaveStateNotice.error">
+            {{ $t('settings.saving_ok') }}
+          </div>
+        </template>
+      </transition>
     </div>
     <div class="panel-body profile-edit">
       <tab-switcher>
diff --git a/src/lib/persisted_state.js b/src/lib/persisted_state.js
index 6f7202ce..e828a74b 100644
--- a/src/lib/persisted_state.js
+++ b/src/lib/persisted_state.js
@@ -84,12 +84,12 @@ export default function createPersistedState ({
             setState(key, reducer(state, paths), storage)
               .then(success => {
                 if (typeof success !== 'undefined') {
-                  if (mutation.type === 'setOption') {
+                  if (mutation.type === 'setOption' || mutation.type === 'setCurrentUser') {
                     store.dispatch('settingsSaved', { success })
                   }
                 }
               }, error => {
-                if (mutation.type === 'setOption') {
+                if (mutation.type === 'setOption' || mutation.type === 'setCurrentUser') {
                   store.dispatch('settingsSaved', { error })
                 }
               })
-- 
cgit v1.2.3-70-g09d2


From a56d2dfeb1c5ddd93c1c23036c0e0f2ca16d273c Mon Sep 17 00:00:00 2001
From: taehoon <th.dev91@gmail.com>
Date: Wed, 13 Feb 2019 07:15:08 -0500
Subject: Add blocks tab with test data to user settings page

---
 src/components/user_settings/user_settings.js  | 14 ++++++++++++--
 src/components/user_settings/user_settings.vue |  4 ++++
 src/i18n/en.json                               |  1 +
 3 files changed, 17 insertions(+), 2 deletions(-)

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

diff --git a/src/components/user_settings/user_settings.js b/src/components/user_settings/user_settings.js
index 904060b2..34ab99c7 100644
--- a/src/components/user_settings/user_settings.js
+++ b/src/components/user_settings/user_settings.js
@@ -1,9 +1,18 @@
-import { unescape } from 'lodash'
+import unescape from 'lodash/unescape'
+import get from 'lodash/get'
 
 import TabSwitcher from '../tab_switcher/tab_switcher.js'
 import ImageCropper from '../image_cropper/image_cropper.vue'
 import StyleSwitcher from '../style_switcher/style_switcher.vue'
 import fileSizeFormatService from '../../services/file_size_format/file_size_format.js'
+import UserList from '../user_list/user_list.vue'
+import withLoadMore from '../../hocs/with_load_more/with_load_more'
+
+const BlockListWithLoadMore = withLoadMore(
+  UserList,
+  (props, $store) => $store.dispatch('addFriends', $store.state.users.currentUser.id),
+  (props, $store) => get($store.getters.userById($store.state.users.currentUser.id), 'friends', [])
+)
 
 const UserSettings = {
   data () {
@@ -41,7 +50,8 @@ const UserSettings = {
   components: {
     StyleSwitcher,
     TabSwitcher,
-    ImageCropper
+    ImageCropper,
+    'block-list': BlockListWithLoadMore
   },
   computed: {
     user () {
diff --git a/src/components/user_settings/user_settings.vue b/src/components/user_settings/user_settings.vue
index 948b5c25..be9f27e7 100644
--- a/src/components/user_settings/user_settings.vue
+++ b/src/components/user_settings/user_settings.vue
@@ -162,6 +162,10 @@
             <h2>{{$t('settings.follow_export_processing')}}</h2>
           </div>
         </div>
+        
+        <div :label="$t('settings.blocks_tab')">
+          <block-list />
+        </div>
       </tab-switcher>
     </div>
   </div>
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 78e8fced..ddde1de2 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -110,6 +110,7 @@
     "avatarRadius": "Avatars",
     "background": "Background",
     "bio": "Bio",
+    "blocks_tab": "Blocks",
     "btnRadius": "Buttons",
     "cBlue": "Blue (Reply, follow)",
     "cGreen": "Green (Retweet)",
-- 
cgit v1.2.3-70-g09d2


From 8c8a6edc7800bac854ef23f29aa87f5b932cb415 Mon Sep 17 00:00:00 2001
From: taehoon <th.dev91@gmail.com>
Date: Wed, 13 Feb 2019 21:08:14 -0500
Subject: Remove pagination support from block-list

---
 src/components/user_settings/user_settings.js           | 10 ++++++----
 src/components/user_settings/user_settings.vue          |  2 +-
 src/modules/users.js                                    | 17 ++++++-----------
 .../entity_normalizer/entity_normalizer.service.js      |  2 +-
 4 files changed, 14 insertions(+), 17 deletions(-)

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

diff --git a/src/components/user_settings/user_settings.js b/src/components/user_settings/user_settings.js
index 621dcd4b..8eade382 100644
--- a/src/components/user_settings/user_settings.js
+++ b/src/components/user_settings/user_settings.js
@@ -7,13 +7,15 @@ import StyleSwitcher from '../style_switcher/style_switcher.vue'
 import fileSizeFormatService from '../../services/file_size_format/file_size_format.js'
 import BlockCard from '../block_card/block_card.vue'
 import withLoadMore from '../../hocs/with_load_more/with_load_more'
+import withSubscription from '../../hocs/with_subscription/with_subscription'
 import withList from '../../hocs/with_list/with_list'
 
-const BlockList = withList(BlockCard, entry => ({ userId: entry.id }))
-const BlockListWithLoadMore = withLoadMore(
+const BlockList = withList(BlockCard, userId => ({ userId }))
+const BlockListWithSubscription = withSubscription(
   BlockList,
   (props, $store) => $store.dispatch('fetchBlocks'),
-  (props, $store) => get($store.state.users.currentUser, 'blocks', [])
+  (props, $store) => get($store.state.users.currentUser, 'blockIds', []),
+  'entries'
 )
 
 const UserSettings = {
@@ -53,7 +55,7 @@ const UserSettings = {
     StyleSwitcher,
     TabSwitcher,
     ImageCropper,
-    'block-list': BlockListWithLoadMore
+    'block-list': BlockListWithSubscription
   },
   computed: {
     user () {
diff --git a/src/components/user_settings/user_settings.vue b/src/components/user_settings/user_settings.vue
index be9f27e7..5cf21815 100644
--- a/src/components/user_settings/user_settings.vue
+++ b/src/components/user_settings/user_settings.vue
@@ -164,7 +164,7 @@
         </div>
         
         <div :label="$t('settings.blocks_tab')">
-          <block-list />
+          <block-list id="123" />
         </div>
       </tab-switcher>
     </div>
diff --git a/src/modules/users.js b/src/modules/users.js
index ce8af68c..1f03b47e 100644
--- a/src/modules/users.js
+++ b/src/modules/users.js
@@ -1,5 +1,5 @@
 import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
-import { compact, map, each, merge, find } from 'lodash'
+import { compact, map, each, merge, find, union } from 'lodash'
 import { set } from 'vue'
 import { registerPushNotifications, unregisterPushNotifications } from '../services/push/push.js'
 import oauthApi from '../services/new_api/oauth'
@@ -85,14 +85,9 @@ export const mutations = {
   addNewUsers (state, users) {
     each(users, (user) => mergeOrAdd(state.users, state.usersObject, user))
   },
-  addBlocks (state, { blocks, page }) {
+  addBlocks (state, blockIds) {
     const user = state.currentUser
-    each(blocks, block => {
-      if (!find(user.blocks, { id: block.id })) {
-        user.blocks.push(block)
-      }
-    })
-    user.blocksPage = page + 1
+    user.blockIds = union(user.blockIds, blockIds)
   },
   setUserForStatus (state, status) {
     status.user = state.usersObject[status.user.id]
@@ -147,10 +142,10 @@ const users = {
         .then((user) => store.commit('addNewUsers', [user]))
     },
     fetchBlocks (store) {
-      const page = store.state.currentUser.blocksPage || 1
-      return store.rootState.api.backendInteractor.fetchBlocks({ page })
+      return store.rootState.api.backendInteractor.fetchBlocks()
         .then((blocks) => {
-          store.commit('addBlocks', { blocks, page })
+          store.commit('addBlocks', map(blocks, 'id'))
+          store.commit('addNewUsers', blocks)
           return blocks
         })
     },
diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
index 1192b6cc..0e1be61e 100644
--- a/src/services/entity_normalizer/entity_normalizer.service.js
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -120,7 +120,7 @@ export const parseUser = (data) => {
   if (data.pleroma) {
     output.follow_request_count = data.pleroma.follow_request_count
   }
-  output.blocks = []
+  output.blockIds = []
 
   return output
 }
-- 
cgit v1.2.3-70-g09d2


From 09315b27804beadb7590f8e908ae9d0eb1d6a992 Mon Sep 17 00:00:00 2001
From: taehoon <th.dev91@gmail.com>
Date: Wed, 13 Feb 2019 21:21:56 -0500
Subject: Add a prop to force-refresh data to withSubscription hoc

---
 src/components/user_settings/user_settings.vue  |  2 +-
 src/hocs/with_subscription/with_subscription.js | 13 +++++++------
 2 files changed, 8 insertions(+), 7 deletions(-)

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

diff --git a/src/components/user_settings/user_settings.vue b/src/components/user_settings/user_settings.vue
index 5cf21815..5bae583c 100644
--- a/src/components/user_settings/user_settings.vue
+++ b/src/components/user_settings/user_settings.vue
@@ -164,7 +164,7 @@
         </div>
         
         <div :label="$t('settings.blocks_tab')">
-          <block-list id="123" />
+          <block-list :refresh="true" />
         </div>
       </tab-switcher>
     </div>
diff --git a/src/hocs/with_subscription/with_subscription.js b/src/hocs/with_subscription/with_subscription.js
index 31fc106f..633517e3 100644
--- a/src/hocs/with_subscription/with_subscription.js
+++ b/src/hocs/with_subscription/with_subscription.js
@@ -1,24 +1,25 @@
 import Vue from 'vue'
-import filter from 'lodash/filter'
+import reject from 'lodash/reject'
 import isEmpty from 'lodash/isEmpty'
+import omit from 'lodash/omit'
 import './with_subscription.scss'
 
 const withSubscription = (Component, fetch, select, contentPropName = 'content') => {
   const originalProps = Component.props || []
-  const props = filter(originalProps, v => v !== 'content')
+  const props = reject(originalProps, v => v === 'content')
 
   return Vue.component('withSubscription', {
     render (createElement) {
       const props = {
         props: {
-          ...this.$props,
+          ...omit(this.$props, 'refresh'),
           [contentPropName]: this.fetchedData
         },
         on: this.$listeners
       }
       return (
         <div class="with-subscription">
-          <Component {...props} />
+          {!this.error && !this.loading && <Component {...props} />}
           <div class="with-subscription-footer">
             {this.error && <a onClick={this.fetchData} class="alert error">{this.$t('general.generic_error')}</a>}
             {!this.error && this.loading && <i class="icon-spin3 animate-spin"/>}
@@ -26,7 +27,7 @@ const withSubscription = (Component, fetch, select, contentPropName = 'content')
         </div>
       )
     },
-    props,
+    props: [...props, 'refresh'],
     data () {
       return {
         loading: false,
@@ -39,7 +40,7 @@ const withSubscription = (Component, fetch, select, contentPropName = 'content')
       }
     },
     created () {
-      if (isEmpty(this.fetchedData)) {
+      if (this.refresh || isEmpty(this.fetchedData)) {
         this.fetchData()
       }
     },
-- 
cgit v1.2.3-70-g09d2


From e91a94ff9c4b9559f53a4b001f8972ceca843771 Mon Sep 17 00:00:00 2001
From: taehoon <th.dev91@gmail.com>
Date: Wed, 13 Feb 2019 22:04:28 -0500
Subject: Add mutes tab

---
 src/components/mute_card/mute_card.js              | 37 ++++++++++++++++++++++
 src/components/mute_card/mute_card.vue             | 24 ++++++++++++++
 src/components/user_settings/user_settings.js      | 13 ++++++--
 src/components/user_settings/user_settings.vue     |  4 +++
 src/i18n/en.json                                   |  1 +
 src/modules/users.js                               | 25 ++++++++++++---
 .../entity_normalizer/entity_normalizer.service.js |  1 +
 7 files changed, 99 insertions(+), 6 deletions(-)
 create mode 100644 src/components/mute_card/mute_card.js
 create mode 100644 src/components/mute_card/mute_card.vue

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

diff --git a/src/components/mute_card/mute_card.js b/src/components/mute_card/mute_card.js
new file mode 100644
index 00000000..5dd0a9e5
--- /dev/null
+++ b/src/components/mute_card/mute_card.js
@@ -0,0 +1,37 @@
+import BasicUserCard from '../basic_user_card/basic_user_card.vue'
+
+const MuteCard = {
+  props: ['userId'],
+  data () {
+    return {
+      progress: false
+    }
+  },
+  computed: {
+    user () {
+      return this.$store.getters.userById(this.userId)
+    },
+    muted () {
+      return this.user.muted
+    }
+  },
+  components: {
+    BasicUserCard
+  },
+  methods: {
+    unmuteUser () {
+      this.progress = true
+      this.$store.dispatch('unmuteUser', this.user.id).then(() => {
+        this.progress = false
+      })
+    },
+    muteUser () {
+      this.progress = true
+      this.$store.dispatch('muteUser', this.user.id).then(() => {
+        this.progress = false
+      })
+    }
+  }
+}
+
+export default MuteCard
diff --git a/src/components/mute_card/mute_card.vue b/src/components/mute_card/mute_card.vue
new file mode 100644
index 00000000..e1bfe20b
--- /dev/null
+++ b/src/components/mute_card/mute_card.vue
@@ -0,0 +1,24 @@
+<template>
+  <basic-user-card :user="user">
+    <template slot="secondary-area">
+      <button class="btn btn-default" @click="unmuteUser" :disabled="progress" v-if="muted">
+        <template v-if="progress">
+          {{ $t('user_card.unmute_progress') }}
+        </template>
+        <template v-else>
+          {{ $t('user_card.unmute') }}
+        </template>
+      </button>
+      <button class="btn btn-default" @click="muteUser" :disabled="progress" v-else>
+        <template v-if="progress">
+          {{ $t('user_card.mute_progress') }}
+        </template>
+        <template v-else>
+          {{ $t('user_card.mute') }}
+        </template>
+      </button>
+    </template>
+  </basic-user-card>
+</template>
+
+<script src="./mute_card.js"></script>
\ No newline at end of file
diff --git a/src/components/user_settings/user_settings.js b/src/components/user_settings/user_settings.js
index 8eade382..8114d5e2 100644
--- a/src/components/user_settings/user_settings.js
+++ b/src/components/user_settings/user_settings.js
@@ -6,7 +6,7 @@ import ImageCropper from '../image_cropper/image_cropper.vue'
 import StyleSwitcher from '../style_switcher/style_switcher.vue'
 import fileSizeFormatService from '../../services/file_size_format/file_size_format.js'
 import BlockCard from '../block_card/block_card.vue'
-import withLoadMore from '../../hocs/with_load_more/with_load_more'
+import MuteCard from '../mute_card/mute_card.vue'
 import withSubscription from '../../hocs/with_subscription/with_subscription'
 import withList from '../../hocs/with_list/with_list'
 
@@ -18,6 +18,14 @@ const BlockListWithSubscription = withSubscription(
   'entries'
 )
 
+const MuteList = withList(MuteCard, userId => ({ userId }))
+const MuteListWithSubscription = withSubscription(
+  MuteList,
+  (props, $store) => $store.dispatch('fetchMutes'),
+  (props, $store) => get($store.state.users.currentUser, 'muteIds', []),
+  'entries'
+)
+
 const UserSettings = {
   data () {
     return {
@@ -55,7 +63,8 @@ const UserSettings = {
     StyleSwitcher,
     TabSwitcher,
     ImageCropper,
-    'block-list': BlockListWithSubscription
+    'block-list': BlockListWithSubscription,
+    'mute-list': MuteListWithSubscription
   },
   computed: {
     user () {
diff --git a/src/components/user_settings/user_settings.vue b/src/components/user_settings/user_settings.vue
index 5bae583c..27f3e7cb 100644
--- a/src/components/user_settings/user_settings.vue
+++ b/src/components/user_settings/user_settings.vue
@@ -166,6 +166,10 @@
         <div :label="$t('settings.blocks_tab')">
           <block-list :refresh="true" />
         </div>
+
+        <div :label="$t('settings.mutes_tab')">
+          <mute-list :refresh="true" />
+        </div>
       </tab-switcher>
     </div>
   </div>
diff --git a/src/i18n/en.json b/src/i18n/en.json
index eeb95f9c..b41b6db9 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -165,6 +165,7 @@
     "lock_account_description": "Restrict your account to approved followers only",
     "loop_video": "Loop videos",
     "loop_video_silent_only": "Loop only videos without sound (i.e. Mastodon's \"gifs\")",
+    "mutes_tab": "Mutes",
     "play_videos_in_modal": "Play videos directly in the media viewer",
     "use_contain_fit": "Don't crop the attachment in thumbnails",
     "name": "Name",
diff --git a/src/modules/users.js b/src/modules/users.js
index 1f03b47e..71201a77 100644
--- a/src/modules/users.js
+++ b/src/modules/users.js
@@ -89,6 +89,10 @@ export const mutations = {
     const user = state.currentUser
     user.blockIds = union(user.blockIds, blockIds)
   },
+  saveMutes (state, ids) {
+    const user = state.currentUser
+    user.muteIds = union(user.muteIds, ids)
+  },
   setUserForStatus (state, status) {
     status.user = state.usersObject[status.user.id]
   },
@@ -157,6 +161,22 @@ const users = {
       return store.rootState.api.backendInteractor.unblockUser(id)
         .then((user) => store.commit('addNewUsers', [user]))
     },
+    fetchMutes (store) {
+      return store.rootState.api.backendInteractor.fetchMutes()
+        .then((mutedUsers) => {
+          each(mutedUsers, (user) => { user.muted = 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]))
+    },
+    unmuteUser (store, id) {
+      return store.state.api.backendInteractor.setUserMute({ id, muted: false })
+        .then((user) => store.commit('addNewUsers', [user]))
+    },
     addFriends ({ rootState, commit }, fetchBy) {
       return new Promise((resolve, reject) => {
         const user = rootState.users.usersObject[fetchBy]
@@ -300,10 +320,7 @@ const users = {
               store.dispatch('startFetching', { timeline: 'friends' })
 
               // Get user mutes and follower info
-              store.rootState.api.backendInteractor.fetchMutes().then((mutedUsers) => {
-                each(mutedUsers, (user) => { user.muted = true })
-                store.commit('addNewUsers', mutedUsers)
-              })
+              store.dispatch('fetchMutes')
 
               // Fetch our friends
               store.rootState.api.backendInteractor.fetchFriends({ id: user.id })
diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
index 0e1be61e..49c83811 100644
--- a/src/services/entity_normalizer/entity_normalizer.service.js
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -121,6 +121,7 @@ export const parseUser = (data) => {
     output.follow_request_count = data.pleroma.follow_request_count
   }
   output.blockIds = []
+  output.muteIds = []
 
   return output
 }
-- 
cgit v1.2.3-70-g09d2


From a5162bd636880f53a8a47fbd547971f2aaead381 Mon Sep 17 00:00:00 2001
From: taehoon <th.dev91@gmail.com>
Date: Thu, 14 Feb 2019 03:16:37 -0500
Subject: Add note for empty state to the lists using slot

---
 src/components/user_settings/user_settings.vue |  8 ++++++--
 src/hocs/with_list/with_list.js                | 11 +++++++++--
 src/hocs/with_list/with_list.scss              |  6 ++++++
 3 files changed, 21 insertions(+), 4 deletions(-)
 create mode 100644 src/hocs/with_list/with_list.scss

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

diff --git a/src/components/user_settings/user_settings.vue b/src/components/user_settings/user_settings.vue
index 27f3e7cb..520a8cbe 100644
--- a/src/components/user_settings/user_settings.vue
+++ b/src/components/user_settings/user_settings.vue
@@ -164,11 +164,15 @@
         </div>
         
         <div :label="$t('settings.blocks_tab')">
-          <block-list :refresh="true" />
+          <block-list :refresh="true">
+            <template slot="empty">No blocks</template>
+          </block-list>
         </div>
 
         <div :label="$t('settings.mutes_tab')">
-          <mute-list :refresh="true" />
+          <mute-list :refresh="true">
+            <template slot="empty">No mutes</template>
+          </mute-list>
         </div>
       </tab-switcher>
     </div>
diff --git a/src/hocs/with_list/with_list.js b/src/hocs/with_list/with_list.js
index e5770cee..896f8fc8 100644
--- a/src/hocs/with_list/with_list.js
+++ b/src/hocs/with_list/with_list.js
@@ -1,5 +1,7 @@
 import Vue from 'vue'
 import map from 'lodash/map'
+import isEmpty from 'lodash/isEmpty'
+import './with_list.scss'
 
 const defaultEntryPropsGetter = entry => ({ entry })
 const defaultKeyGetter = entry => entry.id
@@ -9,6 +11,11 @@ const withList = ({
   getKey = defaultKeyGetter                 // funciton to accept entry and index values and return key prop value
 }) => (ItemComponent) => (
   Vue.component('withList', {
+    props: [
+      'entries',                            // array of entry
+      'entryProps',                         // additional props to be passed into each entry
+      'entryListeners'                      // additional event listeners to be passed into each entry
+    ],
     render (createElement) {
       return (
         <div class="with-list">
@@ -23,10 +30,10 @@ const withList = ({
             }
             return <ItemComponent {...props} />
           })}
+          {isEmpty(this.entries) && this.$slots.empty && <div class="with-list-empty-content faint">{this.$slots.empty}</div>}
         </div>
       )
-    },
-    props: ['entries', 'entryProps', 'entryListeners']
+    }
   })
 )
 
diff --git a/src/hocs/with_list/with_list.scss b/src/hocs/with_list/with_list.scss
new file mode 100644
index 00000000..c6e13d5b
--- /dev/null
+++ b/src/hocs/with_list/with_list.scss
@@ -0,0 +1,6 @@
+.with-list {
+  &-empty-content {
+    text-align: center;
+    padding: 10px;
+  }
+}
\ No newline at end of file
-- 
cgit v1.2.3-70-g09d2


From 395d21290462525209ee4446746f14f996adf6b2 Mon Sep 17 00:00:00 2001
From: taehoon <th.dev91@gmail.com>
Date: Thu, 14 Feb 2019 03:19:07 -0500
Subject: Add new strings to i18n

---
 src/components/user_settings/user_settings.vue | 4 ++--
 src/i18n/en.json                               | 2 ++
 2 files changed, 4 insertions(+), 2 deletions(-)

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

diff --git a/src/components/user_settings/user_settings.vue b/src/components/user_settings/user_settings.vue
index 520a8cbe..e18ca4ee 100644
--- a/src/components/user_settings/user_settings.vue
+++ b/src/components/user_settings/user_settings.vue
@@ -165,13 +165,13 @@
         
         <div :label="$t('settings.blocks_tab')">
           <block-list :refresh="true">
-            <template slot="empty">No blocks</template>
+            <template slot="empty">{{$t('settings.no_blocks')}}</template>
           </block-list>
         </div>
 
         <div :label="$t('settings.mutes_tab')">
           <mute-list :refresh="true">
-            <template slot="empty">No mutes</template>
+            <template slot="empty">{{$t('settings.no_mutes')}}</template>
           </mute-list>
         </div>
       </tab-switcher>
diff --git a/src/i18n/en.json b/src/i18n/en.json
index b41b6db9..93819c2c 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -177,6 +177,8 @@
     "notification_visibility_mentions": "Mentions",
     "notification_visibility_repeats": "Repeats",
     "no_rich_text_description": "Strip rich text formatting from all posts",
+    "no_blocks": "No blocks",
+    "no_mutes": "No mutes",
     "hide_follows_description": "Don't show who I'm following",
     "hide_followers_description": "Don't show who's following me",
     "show_admin_badge": "Show Admin badge in my profile",
-- 
cgit v1.2.3-70-g09d2


From 37eec09b9b63ef494a075776234382a3da25773e Mon Sep 17 00:00:00 2001
From: taehoon <th.dev91@gmail.com>
Date: Wed, 20 Feb 2019 12:36:15 -0500
Subject: Comment out the mutes tab

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

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

diff --git a/src/components/user_settings/user_settings.vue b/src/components/user_settings/user_settings.vue
index e18ca4ee..5c272114 100644
--- a/src/components/user_settings/user_settings.vue
+++ b/src/components/user_settings/user_settings.vue
@@ -162,18 +162,18 @@
             <h2>{{$t('settings.follow_export_processing')}}</h2>
           </div>
         </div>
-        
+
         <div :label="$t('settings.blocks_tab')">
           <block-list :refresh="true">
             <template slot="empty">{{$t('settings.no_blocks')}}</template>
           </block-list>
         </div>
 
-        <div :label="$t('settings.mutes_tab')">
+        <!-- <div :label="$t('settings.mutes_tab')">
           <mute-list :refresh="true">
             <template slot="empty">{{$t('settings.no_mutes')}}</template>
           </mute-list>
-        </div>
+        </div> -->
       </tab-switcher>
     </div>
   </div>
-- 
cgit v1.2.3-70-g09d2


From 22851a3a967cc5364a95c71bda48eccc3808bf41 Mon Sep 17 00:00:00 2001
From: taehoon <th.dev91@gmail.com>
Date: Thu, 21 Feb 2019 20:15:51 -0500
Subject: Remove needless code

---
 src/components/user_settings/user_settings.vue | 6 ------
 1 file changed, 6 deletions(-)

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

diff --git a/src/components/user_settings/user_settings.vue b/src/components/user_settings/user_settings.vue
index 5c272114..983cbda0 100644
--- a/src/components/user_settings/user_settings.vue
+++ b/src/components/user_settings/user_settings.vue
@@ -168,12 +168,6 @@
             <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>
-- 
cgit v1.2.3-70-g09d2


From 2c7406d9a8d8e961b99286f317c6ed0a48427853 Mon Sep 17 00:00:00 2001
From: Maxim Filippov <colixer@gmail.com>
Date: Tue, 12 Feb 2019 21:53:59 +0300
Subject: Add OAuth Tokens management to settings

---
 src/components/user_settings/user_settings.js      | 24 +++++++++++++++
 src/components/user_settings/user_settings.vue     | 36 ++++++++++++++++++++++
 src/i18n/ar.json                                   |  5 +++
 src/i18n/ca.json                                   |  5 +++
 src/i18n/de.json                                   |  5 +++
 src/i18n/en.json                                   |  5 +++
 src/i18n/es.json                                   |  5 +++
 src/i18n/fi.json                                   |  5 +++
 src/i18n/fr.json                                   |  5 +++
 src/i18n/ga.json                                   |  5 +++
 src/i18n/he.json                                   |  5 +++
 src/i18n/it.json                                   |  5 +++
 src/i18n/ja.json                                   |  5 +++
 src/i18n/ko.json                                   |  5 +++
 src/i18n/nb.json                                   |  5 +++
 src/i18n/nl.json                                   |  5 +++
 src/i18n/oc.json                                   |  1 +
 src/i18n/pl.json                                   |  5 +++
 src/i18n/ru.json                                   |  5 +++
 src/i18n/zh.json                                   |  5 +++
 src/main.js                                        |  4 ++-
 src/modules/oauth_tokens.js                        | 26 ++++++++++++++++
 src/services/api/api.service.js                    | 19 ++++++++++++
 .../backend_interactor_service.js                  |  4 +++
 test/unit/specs/modules/users.spec.js              | 10 ++++++
 25 files changed, 208 insertions(+), 1 deletion(-)
 create mode 100644 src/modules/oauth_tokens.js

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

diff --git a/src/components/user_settings/user_settings.js b/src/components/user_settings/user_settings.js
index 06e72112..c7dab754 100644
--- a/src/components/user_settings/user_settings.js
+++ b/src/components/user_settings/user_settings.js
@@ -1,6 +1,10 @@
+<<<<<<< HEAD
 import { compose } from 'vue-compose'
 import unescape from 'lodash/unescape'
 import get from 'lodash/get'
+=======
+import { unescape, truncate } from 'lodash'
+>>>>>>> Add OAuth Tokens management to settings
 
 import TabSwitcher from '../tab_switcher/tab_switcher.js'
 import ImageCropper from '../image_cropper/image_cropper.vue'
@@ -62,6 +66,9 @@ const UserSettings = {
       activeTab: 'profile'
     }
   },
+  created () {
+    this.$store.dispatch('fetchTokens')
+  },
   components: {
     StyleSwitcher,
     TabSwitcher,
@@ -87,8 +94,20 @@ const UserSettings = {
         direct: { selected: this.newDefaultScope === 'direct' }
       }
     },
+<<<<<<< HEAD
     currentSaveStateNotice () {
       return this.$store.state.interface.settings.currentSaveStateNotice
+=======
+    oauthTokens () {
+      return this.$store.state.oauthTokens.tokens.map(oauthToken => {
+        return {
+          id: oauthToken.id,
+          token: truncate(oauthToken.token, { length: 15 }),
+          refreshToken: truncate(oauthToken.refresh_token, { length: 15 }),
+          validUntil: new Date(oauthToken.valid_until).toLocaleDateString()
+        }
+      })
+>>>>>>> Add OAuth Tokens management to settings
     }
   },
   methods: {
@@ -308,6 +327,11 @@ const UserSettings = {
     logout () {
       this.$store.dispatch('logout')
       this.$router.replace('/')
+    },
+    revokeToken (id) {
+      if(confirm('Are you sure?')) {
+        this.$store.dispatch('revokeToken', id)
+      }
     }
   }
 }
diff --git a/src/components/user_settings/user_settings.vue b/src/components/user_settings/user_settings.vue
index 983cbda0..ac75ad86 100644
--- a/src/components/user_settings/user_settings.vue
+++ b/src/components/user_settings/user_settings.vue
@@ -121,6 +121,30 @@
             <p v-if="changePasswordError">{{changePasswordError}}</p>
           </div>
 
+          <div class="setting-item">
+            <h2>{{$t('settings.oauth_tokens')}}</h2>
+            <table class="oauth-tokens">
+              <thead>
+                <tr>
+                  <th>Token</th>
+                  <th>Refresh Token</th>
+                  <th>Valid Until</th>
+                  <th></th>
+                </tr>
+              </thead>
+              <tbody>
+                <tr v-for="oauthToken in oauthTokens" :key="oauthToken.id">
+                  <td>{{oauthToken.token}}</td>
+                  <td>{{oauthToken.refreshToken}}</td>
+                  <td>{{oauthToken.validUntil}}</td>
+                  <td class="actions">
+                    <button class="btn btn-default" @click="revokeToken(oauthToken.id)">Revoke</button>
+                  </td>
+                </tr>
+              </tbody>
+            </table>
+          </div>
+
           <div class="setting-item">
             <h2>{{$t('settings.delete_account')}}</h2>
             <p v-if="!deletingAccount">{{$t('settings.delete_account_description')}}</p>
@@ -213,5 +237,17 @@
     border-radius: $fallback--avatarRadius;
     border-radius: var(--avatarRadius, $fallback--avatarRadius);
   }
+
+  .oauth-tokens {
+    width: 100%;
+
+    th {
+      text-align: left;
+    }
+
+    .actions {
+      text-align: right;
+    }
+  }
 }
 </style>
diff --git a/src/i18n/ar.json b/src/i18n/ar.json
index ac7d0f1a..242dab78 100644
--- a/src/i18n/ar.json
+++ b/src/i18n/ar.json
@@ -134,6 +134,11 @@
         "notification_visibility_mentions": "الإشارات",
         "notification_visibility_repeats": "",
         "nsfw_clickthrough": "",
+        "oauth_tokens": "رموز OAuth",
+        "token": "رمز",
+        "refresh_token": "رمز التحديث",
+        "valid_until": "صالح حتى",
+        "revoke_token": "سحب",
         "panelRadius": "",
         "pause_on_unfocused": "",
         "presets": "النماذج",
diff --git a/src/i18n/ca.json b/src/i18n/ca.json
index fa517e22..d2f285df 100644
--- a/src/i18n/ca.json
+++ b/src/i18n/ca.json
@@ -132,6 +132,11 @@
     "notification_visibility_repeats": "Republica una entrada meva",
     "no_rich_text_description": "Neteja el formatat de text de totes les entrades",
     "nsfw_clickthrough": "Amaga el contingut NSFW darrer d'una imatge clicable",
+    "oauth_tokens": "Llistats OAuth",
+    "token": "Token",
+    "refresh_token": "Actualitza el token",
+    "valid_until": "Vàlid fins",
+    "revoke_token": "Revocar",
     "panelRadius": "Panells",
     "pause_on_unfocused": "Pausa la reproducció en continu quan la pestanya perdi el focus",
     "presets": "Temes",
diff --git a/src/i18n/de.json b/src/i18n/de.json
index d0bfba38..07d44348 100644
--- a/src/i18n/de.json
+++ b/src/i18n/de.json
@@ -159,6 +159,11 @@
     "hide_follows_description": "Zeige nicht, wem ich folge",
     "hide_followers_description": "Zeige nicht, wer mir folgt",
     "nsfw_clickthrough": "Aktiviere ausblendbares Overlay für Anhänge, die als NSFW markiert sind",
+    "oauth_tokens": "OAuth-Token",
+    "token": "Zeichen",
+    "refresh_token": "Token aktualisieren",
+    "valid_until": "Gültig bis",
+    "revoke_token": "Widerrufen",
     "panelRadius": "Panel",
     "pause_on_unfocused": "Streaming pausieren, wenn das Tab nicht fokussiert ist",
     "presets": "Voreinstellungen",
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 8e837d2f..0aeb70ab 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -189,6 +189,11 @@
     "show_admin_badge": "Show Admin badge in my profile",
     "show_moderator_badge": "Show Moderator badge in my profile",
     "nsfw_clickthrough": "Enable clickthrough NSFW attachment hiding",
+    "oauth_tokens": "OAuth tokens",
+    "token": "Token",
+    "refresh_token": "Refresh Token",
+    "valid_until": "Valid Until",
+    "revoke_token": "Revoke",
     "panelRadius": "Panels",
     "pause_on_unfocused": "Pause streaming when tab is not focused",
     "presets": "Presets",
diff --git a/src/i18n/es.json b/src/i18n/es.json
index d14e7a31..167e8c42 100644
--- a/src/i18n/es.json
+++ b/src/i18n/es.json
@@ -171,6 +171,11 @@
     "show_admin_badge": "Mostrar la placa de administrador en mi perfil",
     "show_moderator_badge": "Mostrar la placa de moderador en mi perfil",
     "nsfw_clickthrough": "Activar el clic para ocultar los adjuntos NSFW",
+    "oauth_tokens": "Tokens de OAuth",
+    "token": "Token",
+    "refresh_token": "Actualizar el token",
+    "valid_until": "Válido hasta",
+    "revoke_token": "Revocar",
     "panelRadius": "Paneles",
     "pause_on_unfocused": "Parar la transmisión cuando no estés en foco.",
     "presets": "Por defecto",
diff --git a/src/i18n/fi.json b/src/i18n/fi.json
index a8259ca8..c7a25fe1 100644
--- a/src/i18n/fi.json
+++ b/src/i18n/fi.json
@@ -166,6 +166,11 @@
     "no_rich_text_description": "Älä näytä tekstin muotoilua.",
     "hide_network_description": "Älä näytä seurauksiani tai seuraajiani",
     "nsfw_clickthrough": "Piilota NSFW liitteet klikkauksen taakse",
+    "oauth_tokens": "OAuth-merkit",
+    "token": "Token",
+    "refresh_token": "Päivitä token",
+    "valid_until": "Voimassa asti",
+    "revoke_token": "Peruuttaa",
     "panelRadius": "Ruudut",
     "pause_on_unfocused": "Pysäytä automaattinen viestien näyttö välilehden ollessa pois fokuksesta",
     "presets": "Valmiit teemat",
diff --git a/src/i18n/fr.json b/src/i18n/fr.json
index 129b7d7c..1209556a 100644
--- a/src/i18n/fr.json
+++ b/src/i18n/fr.json
@@ -137,6 +137,11 @@
     "notification_visibility_mentions": "Mentionnés",
     "notification_visibility_repeats": "Partages",
     "nsfw_clickthrough": "Masquer les images marquées comme contenu adulte ou sensible",
+    "oauth_tokens": "Jetons OAuth",
+    "token": "Jeton",
+    "refresh_token": "Refresh Token",
+    "valid_until": "Valable jusque",
+    "revoke_token": "Révoquer",
     "panelRadius": "Fenêtres",
     "pause_on_unfocused": "Suspendre le streaming lorsque l'onglet n'est pas centré",
     "presets": "Thèmes prédéfinis",
diff --git a/src/i18n/ga.json b/src/i18n/ga.json
index 64461202..5be9297a 100644
--- a/src/i18n/ga.json
+++ b/src/i18n/ga.json
@@ -134,6 +134,11 @@
     "notification_visibility_repeats": "Atphostáil",
     "no_rich_text_description": "Bain formáidiú téacs saibhir ó gach post",
     "nsfw_clickthrough": "Cumasaigh an ceangaltán NSFW cliceáil ar an gcnaipe",
+    "oauth_tokens": "Tocanna OAuth",
+    "token": "Token",
+    "refresh_token": "Athnuachan Comórtas",
+    "valid_until": "Bailí Go dtí",
+    "revoke_token": "Athghairm",
     "panelRadius": "Painéil",
     "pause_on_unfocused": "Sruthú ar sos nuair a bhíonn an fócas caillte",
     "presets": "Réamhshocruithe",
diff --git a/src/i18n/he.json b/src/i18n/he.json
index 99ae9551..213e6170 100644
--- a/src/i18n/he.json
+++ b/src/i18n/he.json
@@ -129,6 +129,11 @@
     "notification_visibility_mentions": "אזכורים",
     "notification_visibility_repeats": "חזרות",
     "nsfw_clickthrough": "החל החבאת צירופים לא בטוחים לצפיה בעת עבודה בעזרת לחיצת עכבר",
+    "oauth_tokens": "אסימוני OAuth",
+    "token": "אסימון",
+    "refresh_token": "רענון האסימון",
+    "valid_until": "בתוקף עד",
+    "revoke_token": "בטל",
     "panelRadius": "פאנלים",
     "pause_on_unfocused": "השהה זרימת הודעות כשהחלון לא בפוקוס",
     "presets": "ערכים קבועים מראש",
diff --git a/src/i18n/it.json b/src/i18n/it.json
index 8f69e7c1..385d21aa 100644
--- a/src/i18n/it.json
+++ b/src/i18n/it.json
@@ -93,6 +93,11 @@
     "notification_visibility_mentions": "Menzioni",
     "notification_visibility_repeats": "Condivisioni",
     "no_rich_text_description": "Togli la formattazione del testo da tutti i post",
+    "oauth_tokens": "Token OAuth",
+    "token": "Token",
+    "refresh_token": "Aggiorna token",
+    "valid_until": "Valido fino a",
+    "revoke_token": "Revocare",
     "panelRadius": "Pannelli",
     "pause_on_unfocused": "Metti in pausa l'aggiornamento continuo quando la scheda non è in primo piano",
     "presets": "Valori predefiniti",
diff --git a/src/i18n/ja.json b/src/i18n/ja.json
index 7849aa20..b51fa7fd 100644
--- a/src/i18n/ja.json
+++ b/src/i18n/ja.json
@@ -171,6 +171,11 @@
     "show_admin_badge": "アドミンのしるしをみる",
     "show_moderator_badge": "モデレーターのしるしをみる",
     "nsfw_clickthrough": "NSFWなファイルをかくす",
+    "oauth_tokens": "OAuthトークン",
+    "token": "トークン",
+    "refresh_token": "トークンを更新",
+    "valid_until": "まで有効",
+    "revoke_token": "取り消す",
     "panelRadius": "パネル",
     "pause_on_unfocused": "タブにフォーカスがないときストリーミングをとめる",
     "presets": "プリセット",
diff --git a/src/i18n/ko.json b/src/i18n/ko.json
index f9e4dfa3..336e464f 100644
--- a/src/i18n/ko.json
+++ b/src/i18n/ko.json
@@ -159,6 +159,11 @@
     "hide_follows_description": "내가 팔로우하는 사람을 표시하지 않음",
     "hide_followers_description": "나를 따르는 사람을 보여주지 마라.",
     "nsfw_clickthrough": "NSFW 이미지 \"클릭해서 보이기\"를 활성화",
+    "oauth_tokens": "OAuth 토큰",
+    "token": "토큰",
+    "refresh_token": "토큰 새로 고침",
+    "valid_until": "까지 유효하다",
+    "revoke_token": "취소",
     "panelRadius": "패널",
     "pause_on_unfocused": "탭이 활성 상태가 아닐 때 스트리밍 멈추기",
     "presets": "프리셋",
diff --git a/src/i18n/nb.json b/src/i18n/nb.json
index 0f4dca58..39e054f7 100644
--- a/src/i18n/nb.json
+++ b/src/i18n/nb.json
@@ -132,6 +132,11 @@
     "notification_visibility_repeats": "Gjentakelser",
     "no_rich_text_description": "Fjern all formatering fra statuser",
     "nsfw_clickthrough": "Krev trykk for å vise statuser som kan være upassende",
+    "oauth_tokens": "OAuth Tokens",
+    "token": "Pollett",
+    "refresh_token": "Refresh Token",
+    "valid_until": "Gyldig til",
+    "revoke_token": "Tilbakekall",
     "panelRadius": "Panel",
     "pause_on_unfocused": "Stopp henting av poster når vinduet ikke er i fokus",
     "presets": "Forhåndsdefinerte tema",
diff --git a/src/i18n/nl.json b/src/i18n/nl.json
index bb388a90..799e22b9 100644
--- a/src/i18n/nl.json
+++ b/src/i18n/nl.json
@@ -159,6 +159,11 @@
     "no_rich_text_description": "Strip rich text formattering van alle posts",
     "hide_network_description": "Toon niet wie mij volgt en wie ik volg.",
     "nsfw_clickthrough": "Schakel doorklikbaar verbergen van NSFW bijlages in",
+    "oauth_tokens": "OAuth-tokens",
+    "token": "Token",
+    "refresh_token": "Token vernieuwen",
+    "valid_until": "Geldig tot",
+    "revoke_token": "Intrekken",
     "panelRadius": "Panelen",
     "pause_on_unfocused": "Pauzeer streamen wanneer de tab niet gefocused is",
     "presets": "Presets",
diff --git a/src/i18n/oc.json b/src/i18n/oc.json
index 2ce666c6..db66bb98 100644
--- a/src/i18n/oc.json
+++ b/src/i18n/oc.json
@@ -142,6 +142,7 @@
     "notification_visibility_mentions": "Mencions",
     "notification_visibility_repeats": "Repeticions",
     "no_rich_text_description": "Netejar lo format tèxte de totas las publicacions",
+    "oauth_tokens": "Llistats OAuth",
     "pause_on_unfocused": "Pausar la difusion quand l’onglet es pas seleccionat",
     "profile_tab": "Perfil",
     "replies_in_timeline": "Responsas del flux",
diff --git a/src/i18n/pl.json b/src/i18n/pl.json
index a3952d4f..2e1d7488 100644
--- a/src/i18n/pl.json
+++ b/src/i18n/pl.json
@@ -86,6 +86,11 @@
     "name_bio": "Imię i bio",
     "new_password": "Nowe hasło",
     "nsfw_clickthrough": "Włącz domyślne ukrywanie załączników o treści nieprzyzwoitej (NSFW)",
+    "oauth_tokens": "Tokeny OAuth",
+    "token": "Token",
+    "refresh_token": "Odśwież token",
+    "valid_until": "Ważne do",
+    "revoke_token": "Odwołać",
     "panelRadius": "Panele",
     "presets": "Gotowe motywy",
     "profile_background": "Tło profilu",
diff --git a/src/i18n/ru.json b/src/i18n/ru.json
index 4b0bd4b4..6799cc96 100644
--- a/src/i18n/ru.json
+++ b/src/i18n/ru.json
@@ -132,6 +132,11 @@
     "show_admin_badge": "Показывать значок администратора в моем профиле",
     "show_moderator_badge": "Показывать значок модератора в моем профиле",
     "nsfw_clickthrough": "Включить скрытие NSFW вложений",
+    "oauth_tokens": "OAuth токены",
+    "token": "Токен",
+    "refresh_token": "Рефреш токен",
+    "valid_until": "Годен до",
+    "revoke_token": "Удалить",
     "panelRadius": "Панели",
     "pause_on_unfocused": "Приостановить загрузку когда вкладка не в фокусе",
     "presets": "Пресеты",
diff --git a/src/i18n/zh.json b/src/i18n/zh.json
index 7ad23c57..089a98e2 100644
--- a/src/i18n/zh.json
+++ b/src/i18n/zh.json
@@ -134,6 +134,11 @@
     "notification_visibility_repeats": "转发",
     "no_rich_text_description": "不显示富文本格式",
     "nsfw_clickthrough": "将不和谐附件隐藏，点击才能打开",
+    "oauth_tokens": "OAuth令牌",
+    "token": "代币",
+    "refresh_token": "刷新令牌",
+    "valid_until": "有效期至",
+    "revoke_token": "撤消",
     "panelRadius": "面板",
     "pause_on_unfocused": "在离开页面时暂停时间线推送",
     "presets": "预置",
diff --git a/src/main.js b/src/main.js
index adeb0550..2844194e 100644
--- a/src/main.js
+++ b/src/main.js
@@ -11,6 +11,7 @@ import configModule from './modules/config.js'
 import chatModule from './modules/chat.js'
 import oauthModule from './modules/oauth.js'
 import mediaViewerModule from './modules/media_viewer.js'
+import oauthTokensModule from './modules/oauth_tokens.js'
 
 import VueTimeago from 'vue-timeago'
 import VueI18n from 'vue-i18n'
@@ -64,7 +65,8 @@ createPersistedState(persistedStateOptions).then((persistedState) => {
       config: configModule,
       chat: chatModule,
       oauth: oauthModule,
-      mediaViewer: mediaViewerModule
+      mediaViewer: mediaViewerModule,
+      oauthTokens: oauthTokensModule
     },
     plugins: [persistedState, pushNotifications],
     strict: false // Socket modifies itself, let's ignore this for now.
diff --git a/src/modules/oauth_tokens.js b/src/modules/oauth_tokens.js
new file mode 100644
index 00000000..00ac1431
--- /dev/null
+++ b/src/modules/oauth_tokens.js
@@ -0,0 +1,26 @@
+const oauthTokens = {
+  state: {
+    tokens: []
+  },
+  actions: {
+    fetchTokens ({rootState, commit}) {
+      rootState.api.backendInteractor.fetchOAuthTokens().then((tokens) => {
+        commit('swapTokens', tokens)
+      })
+    },
+    revokeToken ({rootState, commit, state}, id) {
+      rootState.api.backendInteractor.revokeOAuthToken(id).then((response) => {
+        if (response.status === 201) {
+          commit('swapTokens', state.tokens.filter(token => token.id !== id))
+        }
+      })
+    }
+  },
+  mutations: {
+    swapTokens (state, tokens) {
+      state.tokens = tokens
+    }
+  }
+}
+
+export default oauthTokens
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index 3d2e8823..7b04343d 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -531,6 +531,23 @@ const fetchBlocks = ({page, credentials}) => {
   })
 }
 
+const fetchOAuthTokens = ({credentials}) => {
+  const url = '/api/oauth_tokens.json'
+
+  return fetch(url, {
+    headers: authHeaders(credentials)
+  }).then((data) => data.json())
+}
+
+const revokeOAuthToken = ({id, credentials}) => {
+  const url = `/api/oauth_tokens/${id}`
+
+  return fetch(url, {
+    headers: authHeaders(credentials),
+    method: 'DELETE'
+  })
+}
+
 const suggestions = ({credentials}) => {
   return fetch(SUGGESTIONS_URL, {
     headers: authHeaders(credentials)
@@ -573,6 +590,8 @@ const apiService = {
   setUserMute,
   fetchMutes,
   fetchBlocks,
+  fetchOAuthTokens,
+  revokeOAuthToken,
   register,
   getCaptcha,
   updateAvatar,
diff --git a/src/services/backend_interactor_service/backend_interactor_service.js b/src/services/backend_interactor_service/backend_interactor_service.js
index 43c914d9..2278cd45 100644
--- a/src/services/backend_interactor_service/backend_interactor_service.js
+++ b/src/services/backend_interactor_service/backend_interactor_service.js
@@ -65,6 +65,8 @@ const backendInteractorService = (credentials) => {
   const fetchMutes = () => apiService.fetchMutes({credentials})
   const fetchBlocks = (params) => apiService.fetchBlocks({credentials, ...params})
   const fetchFollowRequests = () => apiService.fetchFollowRequests({credentials})
+  const fetchOAuthTokens = () => apiService.fetchOAuthTokens({credentials})
+  const revokeOAuthToken = (id) => apiService.revokeOAuthToken({id, credentials})
 
   const getCaptcha = () => apiService.getCaptcha()
   const register = (params) => apiService.register(params)
@@ -96,6 +98,8 @@ const backendInteractorService = (credentials) => {
     setUserMute,
     fetchMutes,
     fetchBlocks,
+    fetchOAuthTokens,
+    revokeOAuthToken,
     register,
     getCaptcha,
     updateAvatar,
diff --git a/test/unit/specs/modules/users.spec.js b/test/unit/specs/modules/users.spec.js
index 4d49ee24..96ae845b 100644
--- a/test/unit/specs/modules/users.spec.js
+++ b/test/unit/specs/modules/users.spec.js
@@ -32,6 +32,16 @@ describe('The users module', () => {
 
       expect(user.muted).to.eql(false)
     })
+
+    it('sets oauth tokens', () => {
+      const state = cloneDeep(defaultState)
+      const tokens = [{ id: 1, token: 'bar' }]
+
+      mutations.addOAuthTokens(state, tokens)
+
+      expect(state.oauthTokens).to.have.length(1)
+      expect(state.oauthTokens).to.eql(tokens)
+    })
   })
 
   describe('getUserByName', () => {
-- 
cgit v1.2.3-70-g09d2


From afbe524a2e7fa7002c4b1f5883e08b2d135a2eda Mon Sep 17 00:00:00 2001
From: Maxim Filippov <colixer@gmail.com>
Date: Tue, 12 Feb 2019 22:07:10 +0300
Subject: use translations

---
 src/components/user_settings/user_settings.vue | 10 ++++++----
 1 file changed, 6 insertions(+), 4 deletions(-)

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

diff --git a/src/components/user_settings/user_settings.vue b/src/components/user_settings/user_settings.vue
index ac75ad86..116c1335 100644
--- a/src/components/user_settings/user_settings.vue
+++ b/src/components/user_settings/user_settings.vue
@@ -126,9 +126,9 @@
             <table class="oauth-tokens">
               <thead>
                 <tr>
-                  <th>Token</th>
-                  <th>Refresh Token</th>
-                  <th>Valid Until</th>
+                  <th>{{$t('settings.token')}}</th>
+                  <th>{{$t('settings.refresh_token')}}</th>
+                  <th>{{$t('settings.valid_until')}}</th>
                   <th></th>
                 </tr>
               </thead>
@@ -138,7 +138,9 @@
                   <td>{{oauthToken.refreshToken}}</td>
                   <td>{{oauthToken.validUntil}}</td>
                   <td class="actions">
-                    <button class="btn btn-default" @click="revokeToken(oauthToken.id)">Revoke</button>
+                    <button class="btn btn-default" @click="revokeToken(oauthToken.id)">
+                      {{$t('settings.revoke_token')}}
+                    </button>
                   </td>
                 </tr>
               </tbody>
-- 
cgit v1.2.3-70-g09d2


From c71f411ad6593a496a041505f8a6642a4ae55eea Mon Sep 17 00:00:00 2001
From: Maxim Filippov <colixer@gmail.com>
Date: Mon, 18 Feb 2019 00:01:18 +0300
Subject: Show only "app_name" and "valid_until" (OAuth tokens table)

---
 src/components/user_settings/user_settings.js  | 12 ++----------
 src/components/user_settings/user_settings.vue |  6 ++----
 src/i18n/en.json                               |  1 +
 3 files changed, 5 insertions(+), 14 deletions(-)

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

diff --git a/src/components/user_settings/user_settings.js b/src/components/user_settings/user_settings.js
index 5e972d82..e7a961f1 100644
--- a/src/components/user_settings/user_settings.js
+++ b/src/components/user_settings/user_settings.js
@@ -1,11 +1,6 @@
-<<<<<<< HEAD
 import { compose } from 'vue-compose'
 import unescape from 'lodash/unescape'
 import get from 'lodash/get'
-=======
-import { unescape, truncate } from 'lodash'
->>>>>>> Add OAuth Tokens management to settings
-
 import TabSwitcher from '../tab_switcher/tab_switcher.js'
 import ImageCropper from '../image_cropper/image_cropper.vue'
 import StyleSwitcher from '../style_switcher/style_switcher.vue'
@@ -94,20 +89,17 @@ const UserSettings = {
         direct: { selected: this.newDefaultScope === 'direct' }
       }
     },
-<<<<<<< HEAD
     currentSaveStateNotice () {
       return this.$store.state.interface.settings.currentSaveStateNotice
-=======
+    },
     oauthTokens () {
       return this.$store.state.oauthTokens.tokens.map(oauthToken => {
         return {
           id: oauthToken.id,
-          token: truncate(oauthToken.token, { length: 15 }),
-          refreshToken: truncate(oauthToken.refresh_token, { length: 15 }),
+          appName: oauthToken.app_name,
           validUntil: new Date(oauthToken.valid_until).toLocaleDateString()
         }
       })
->>>>>>> Add OAuth Tokens management to settings
     }
   },
   methods: {
diff --git a/src/components/user_settings/user_settings.vue b/src/components/user_settings/user_settings.vue
index 116c1335..a1123638 100644
--- a/src/components/user_settings/user_settings.vue
+++ b/src/components/user_settings/user_settings.vue
@@ -126,16 +126,14 @@
             <table class="oauth-tokens">
               <thead>
                 <tr>
-                  <th>{{$t('settings.token')}}</th>
-                  <th>{{$t('settings.refresh_token')}}</th>
+                  <th>{{$t('settings.app_name')}}</th>
                   <th>{{$t('settings.valid_until')}}</th>
                   <th></th>
                 </tr>
               </thead>
               <tbody>
                 <tr v-for="oauthToken in oauthTokens" :key="oauthToken.id">
-                  <td>{{oauthToken.token}}</td>
-                  <td>{{oauthToken.refreshToken}}</td>
+                  <td>{{oauthToken.appName}}</td>
                   <td>{{oauthToken.validUntil}}</td>
                   <td class="actions">
                     <button class="btn btn-default" @click="revokeToken(oauthToken.id)">
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 0aeb70ab..df8f6f6c 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -106,6 +106,7 @@
     }
   },
   "settings": {
+    "app_name": "App name",
     "attachmentRadius": "Attachments",
     "attachments": "Attachments",
     "autoload": "Enable automatic loading when scrolled to the bottom",
-- 
cgit v1.2.3-70-g09d2

