From 7a013ac39392ef251c0789f27dd4660dcd30bd6d Mon Sep 17 00:00:00 2001
From: Shpuld Shpludson <shp@cock.li>
Date: Wed, 15 Jan 2020 20:22:54 +0000
Subject: [PATCH] Implement domain mutes v2

---
 .../domain_mute_card/domain_mute_card.js      |  15 ++
 .../domain_mute_card/domain_mute_card.vue     |  38 +++++
 src/components/user_settings/user_settings.js |  19 ++-
 .../user_settings/user_settings.vue           | 161 +++++++++++++-----
 src/i18n/en.json                              |   9 +
 src/modules/users.js                          |  44 +++++
 src/services/api/api.service.js               |  28 ++-
 7 files changed, 265 insertions(+), 49 deletions(-)
 create mode 100644 src/components/domain_mute_card/domain_mute_card.js
 create mode 100644 src/components/domain_mute_card/domain_mute_card.vue

diff --git a/src/components/domain_mute_card/domain_mute_card.js b/src/components/domain_mute_card/domain_mute_card.js
new file mode 100644
index 00000000..c8e838ba
--- /dev/null
+++ b/src/components/domain_mute_card/domain_mute_card.js
@@ -0,0 +1,15 @@
+import ProgressButton from '../progress_button/progress_button.vue'
+
+const DomainMuteCard = {
+  props: ['domain'],
+  components: {
+    ProgressButton
+  },
+  methods: {
+    unmuteDomain () {
+      return this.$store.dispatch('unmuteDomain', this.domain)
+    }
+  }
+}
+
+export default DomainMuteCard
diff --git a/src/components/domain_mute_card/domain_mute_card.vue b/src/components/domain_mute_card/domain_mute_card.vue
new file mode 100644
index 00000000..567d81c5
--- /dev/null
+++ b/src/components/domain_mute_card/domain_mute_card.vue
@@ -0,0 +1,38 @@
+<template>
+  <div class="domain-mute-card">
+    <div class="domain-mute-card-domain">
+      {{ domain }}
+    </div>
+    <ProgressButton
+      :click="unmuteDomain"
+      class="btn btn-default"
+    >
+      {{ $t('domain_mute_card.unmute') }}
+      <template slot="progress">
+        {{ $t('domain_mute_card.unmute_progress') }}
+      </template>
+    </ProgressButton>
+  </div>
+</template>
+
+<script src="./domain_mute_card.js"></script>
+
+<style lang="scss">
+.domain-mute-card {
+  flex: 1 0;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 0.6em 1em 0.6em 0;
+
+  &-domain {
+    margin-right: 1em;
+    overflow: hidden;
+    text-overflow: ellipsis;
+  }
+
+  button {
+    width: 10em;
+  }
+}
+</style>
diff --git a/src/components/user_settings/user_settings.js b/src/components/user_settings/user_settings.js
index d5d671e4..1709b48f 100644
--- a/src/components/user_settings/user_settings.js
+++ b/src/components/user_settings/user_settings.js
@@ -9,6 +9,7 @@ import ScopeSelector from '../scope_selector/scope_selector.vue'
 import fileSizeFormatService from '../../services/file_size_format/file_size_format.js'
 import BlockCard from '../block_card/block_card.vue'
 import MuteCard from '../mute_card/mute_card.vue'
+import DomainMuteCard from '../domain_mute_card/domain_mute_card.vue'
 import SelectableList from '../selectable_list/selectable_list.vue'
 import ProgressButton from '../progress_button/progress_button.vue'
 import EmojiInput from '../emoji_input/emoji_input.vue'
@@ -32,6 +33,12 @@ const MuteList = withSubscription({
   childPropName: 'items'
 })(SelectableList)
 
+const DomainMuteList = withSubscription({
+  fetch: (props, $store) => $store.dispatch('fetchDomainMutes'),
+  select: (props, $store) => get($store.state.users.currentUser, 'domainMutes', []),
+  childPropName: 'items'
+})(SelectableList)
+
 const UserSettings = {
   data () {
     return {
@@ -67,7 +74,8 @@ const UserSettings = {
       changedPassword: false,
       changePasswordError: false,
       activeTab: 'profile',
-      notificationSettings: this.$store.state.users.currentUser.notification_settings
+      notificationSettings: this.$store.state.users.currentUser.notification_settings,
+      newDomainToMute: ''
     }
   },
   created () {
@@ -80,10 +88,12 @@ const UserSettings = {
     ImageCropper,
     BlockList,
     MuteList,
+    DomainMuteList,
     EmojiInput,
     Autosuggest,
     BlockCard,
     MuteCard,
+    DomainMuteCard,
     ProgressButton,
     Importer,
     Exporter,
@@ -365,6 +375,13 @@ const UserSettings = {
     unmuteUsers (ids) {
       return this.$store.dispatch('unmuteUsers', ids)
     },
+    unmuteDomains (domains) {
+      return this.$store.dispatch('unmuteDomains', domains)
+    },
+    muteDomain () {
+      return this.$store.dispatch('muteDomain', this.newDomainToMute)
+        .then(() => { this.newDomainToMute = '' })
+    },
     identity (value) {
       return value
     }
diff --git a/src/components/user_settings/user_settings.vue b/src/components/user_settings/user_settings.vue
index 3f1982a6..2222c293 100644
--- a/src/components/user_settings/user_settings.vue
+++ b/src/components/user_settings/user_settings.vue
@@ -509,59 +509,114 @@
         </div>
 
         <div :label="$t('settings.mutes_tab')">
-          <div class="profile-edit-usersearch-wrapper">
-            <Autosuggest
-              :filter="filterUnMutedUsers"
-              :query="queryUserIds"
-              :placeholder="$t('settings.search_user_to_mute')"
-            >
-              <MuteCard
-                slot-scope="row"
-                :user-id="row.item"
-              />
-            </Autosuggest>
-          </div>
-          <MuteList
-            :refresh="true"
-            :get-key="identity"
-          >
-            <template
-              slot="header"
-              slot-scope="{selected}"
-            >
-              <div class="profile-edit-bulk-actions">
-                <ProgressButton
-                  v-if="selected.length > 0"
-                  class="btn btn-default"
-                  :click="() => muteUsers(selected)"
+          <tab-switcher>
+            <div label="Users">
+              <div class="profile-edit-usersearch-wrapper">
+                <Autosuggest
+                  :filter="filterUnMutedUsers"
+                  :query="queryUserIds"
+                  :placeholder="$t('settings.search_user_to_mute')"
                 >
-                  {{ $t('user_card.mute') }}
-                  <template slot="progress">
-                    {{ $t('user_card.mute_progress') }}
-                  </template>
-                </ProgressButton>
-                <ProgressButton
-                  v-if="selected.length > 0"
-                  class="btn btn-default"
-                  :click="() => unmuteUsers(selected)"
+                  <MuteCard
+                    slot-scope="row"
+                    :user-id="row.item"
+                  />
+                </Autosuggest>
+              </div>
+              <MuteList
+                :refresh="true"
+                :get-key="identity"
+              >
+                <template
+                  slot="header"
+                  slot-scope="{selected}"
                 >
-                  {{ $t('user_card.unmute') }}
+                  <div class="profile-edit-bulk-actions">
+                    <ProgressButton
+                      v-if="selected.length > 0"
+                      class="btn btn-default"
+                      :click="() => muteUsers(selected)"
+                    >
+                      {{ $t('user_card.mute') }}
+                      <template slot="progress">
+                        {{ $t('user_card.mute_progress') }}
+                      </template>
+                    </ProgressButton>
+                    <ProgressButton
+                      v-if="selected.length > 0"
+                      class="btn btn-default"
+                      :click="() => unmuteUsers(selected)"
+                    >
+                      {{ $t('user_card.unmute') }}
+                      <template slot="progress">
+                        {{ $t('user_card.unmute_progress') }}
+                      </template>
+                    </ProgressButton>
+                  </div>
+                </template>
+                <template
+                  slot="item"
+                  slot-scope="{item}"
+                >
+                  <MuteCard :user-id="item" />
+                </template>
+                <template slot="empty">
+                  {{ $t('settings.no_mutes') }}
+                </template>
+              </MuteList>
+            </div>
+
+            <div :label="$t('settings.domain_mutes')">
+              <div class="profile-edit-domain-mute-form">
+                <input
+                  v-model="newDomainToMute"
+                  :placeholder="$t('settings.type_domains_to_mute')"
+                  type="text"
+                  @keyup.enter="muteDomain"
+                >
+                <ProgressButton
+                  class="btn btn-default"
+                  :click="muteDomain"
+                >
+                  {{ $t('domain_mute_card.mute') }}
                   <template slot="progress">
-                    {{ $t('user_card.unmute_progress') }}
+                    {{ $t('domain_mute_card.mute_progress') }}
                   </template>
                 </ProgressButton>
               </div>
-            </template>
-            <template
-              slot="item"
-              slot-scope="{item}"
-            >
-              <MuteCard :user-id="item" />
-            </template>
-            <template slot="empty">
-              {{ $t('settings.no_mutes') }}
-            </template>
-          </MuteList>
+              <DomainMuteList
+                :refresh="true"
+                :get-key="identity"
+              >
+                <template
+                  slot="header"
+                  slot-scope="{selected}"
+                >
+                  <div class="profile-edit-bulk-actions">
+                    <ProgressButton
+                      v-if="selected.length > 0"
+                      class="btn btn-default"
+                      :click="() => unmuteDomains(selected)"
+                    >
+                      {{ $t('domain_mute_card.unmute') }}
+                      <template slot="progress">
+                        {{ $t('domain_mute_card.unmute_progress') }}
+                      </template>
+                    </ProgressButton>
+                  </div>
+                </template>
+                <template
+                  slot="item"
+                  slot-scope="{item}"
+                >
+                  <DomainMuteCard :domain="item" />
+                </template>
+                <template slot="empty">
+                  {{ $t('settings.no_mutes') }}
+                </template>
+              </DomainMuteList>
+            </div>
+          </tab-switcher>
         </div>
       </tab-switcher>
     </div>
@@ -639,6 +694,18 @@
     }
   }
 
+  &-domain-mute-form {
+    padding: 1em;
+    display: flex;
+    flex-direction: column;
+
+    button {
+      align-self: flex-end;
+      margin-top: 1em;
+      width: 10em;
+    }
+  }
+
   .setting-subitem {
     margin-left: 1.75em;
   }
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 75d66b9f..31f4ac24 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -21,6 +21,12 @@
   "chat": {
     "title": "Chat"
   },
+  "domain_mute_card": {
+    "mute": "Mute",
+    "mute_progress": "Muting...",
+    "unmute": "Unmute",
+    "unmute_progress": "Unmuting..."
+  },
   "exporter": {
     "export": "Export",
     "processing": "Processing, you'll soon be asked to download your file"
@@ -264,6 +270,7 @@
     "delete_account_error": "There was an issue deleting your account. If this persists please contact your instance administrator.",
     "delete_account_instructions": "Type your password in the input below to confirm account deletion.",
     "discoverable": "Allow discovery of this account in search results and other services",
+    "domain_mutes": "Domains",
     "avatar_size_instruction": "The recommended minimum size for avatar images is 150x150 pixels.",
     "pad_emoji": "Pad emoji with spaces when adding from picker",
     "export_theme": "Save preset",
@@ -361,6 +368,7 @@
     "post_status_content_type": "Post status content type",
     "stop_gifs": "Play-on-hover GIFs",
     "streaming": "Enable automatic streaming of new posts when scrolled to the top",
+    "user_mutes": "Users",
     "useStreamingApi": "Receive posts and notifications real-time",
     "useStreamingApiWarning": "(Not recommended, experimental, known to skip posts)",
     "text": "Text",
@@ -369,6 +377,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",
+    "type_domains_to_mute": "Type in domains to mute",
     "upload_a_photo": "Upload a photo",
     "user_settings": "User Settings",
     "values": {
diff --git a/src/modules/users.js b/src/modules/users.js
index b9ed0efa..ce3e595d 100644
--- a/src/modules/users.js
+++ b/src/modules/users.js
@@ -72,6 +72,16 @@ const showReblogs = (store, userId) => {
     .then((relationship) => store.commit('updateUserRelationship', [relationship]))
 }
 
+const muteDomain = (store, domain) => {
+  return store.rootState.api.backendInteractor.muteDomain({ domain })
+    .then(() => store.commit('addDomainMute', domain))
+}
+
+const unmuteDomain = (store, domain) => {
+  return store.rootState.api.backendInteractor.unmuteDomain({ domain })
+    .then(() => store.commit('removeDomainMute', domain))
+}
+
 export const mutations = {
   setMuted (state, { user: { id }, muted }) {
     const user = state.usersObject[id]
@@ -177,6 +187,20 @@ export const mutations = {
       state.currentUser.muteIds.push(muteId)
     }
   },
+  saveDomainMutes (state, domainMutes) {
+    state.currentUser.domainMutes = domainMutes
+  },
+  addDomainMute (state, domain) {
+    if (state.currentUser.domainMutes.indexOf(domain) === -1) {
+      state.currentUser.domainMutes.push(domain)
+    }
+  },
+  removeDomainMute (state, domain) {
+    const index = state.currentUser.domainMutes.indexOf(domain)
+    if (index !== -1) {
+      state.currentUser.domainMutes.splice(index, 1)
+    }
+  },
   setPinnedToUser (state, status) {
     const user = state.usersObject[status.user.id]
     const index = user.pinnedStatusIds.indexOf(status.id)
@@ -297,6 +321,25 @@ const users = {
     unmuteUsers (store, ids = []) {
       return Promise.all(ids.map(id => unmuteUser(store, id)))
     },
+    fetchDomainMutes (store) {
+      return store.rootState.api.backendInteractor.fetchDomainMutes()
+        .then((domainMutes) => {
+          store.commit('saveDomainMutes', domainMutes)
+          return domainMutes
+        })
+    },
+    muteDomain (store, domain) {
+      return muteDomain(store, domain)
+    },
+    unmuteDomain (store, domain) {
+      return unmuteDomain(store, domain)
+    },
+    muteDomains (store, domains = []) {
+      return Promise.all(domains.map(domain => muteDomain(store, domain)))
+    },
+    unmuteDomains (store, domain = []) {
+      return Promise.all(domain.map(domain => unmuteDomain(store, domain)))
+    },
     fetchFriends ({ rootState, commit }, id) {
       const user = rootState.users.usersObject[id]
       const maxId = last(user.friendIds)
@@ -460,6 +503,7 @@ const users = {
               user.credentials = accessToken
               user.blockIds = []
               user.muteIds = []
+              user.domainMutes = []
               commit('setCurrentUser', user)
               commit('addNewUsers', [user])
 
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index ef0267aa..dcbedd8b 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -72,6 +72,7 @@ const MASTODON_MUTE_CONVERSATION = id => `/api/v1/statuses/${id}/mute`
 const MASTODON_UNMUTE_CONVERSATION = id => `/api/v1/statuses/${id}/unmute`
 const MASTODON_SEARCH_2 = `/api/v2/search`
 const MASTODON_USER_SEARCH_URL = '/api/v1/accounts/search'
+const MASTODON_DOMAIN_BLOCKS_URL = '/api/v1/domain_blocks'
 const MASTODON_STREAMING = '/api/v1/streaming'
 
 const oldfetch = window.fetch
@@ -948,6 +949,28 @@ const search2 = ({ credentials, q, resolve, limit, offset, following }) => {
     })
 }
 
+const fetchDomainMutes = ({ credentials }) => {
+  return promisedRequest({ url: MASTODON_DOMAIN_BLOCKS_URL, credentials })
+}
+
+const muteDomain = ({ domain, credentials }) => {
+  return promisedRequest({
+    url: MASTODON_DOMAIN_BLOCKS_URL,
+    method: 'POST',
+    payload: { domain },
+    credentials
+  })
+}
+
+const unmuteDomain = ({ domain, credentials }) => {
+  return promisedRequest({
+    url: MASTODON_DOMAIN_BLOCKS_URL,
+    method: 'DELETE',
+    payload: { domain },
+    credentials
+  })
+}
+
 export const getMastodonSocketURI = ({ credentials, stream, args = {} }) => {
   return Object.entries({
     ...(credentials
@@ -1110,7 +1133,10 @@ const apiService = {
   reportUser,
   updateNotificationSettings,
   search2,
-  searchUsers
+  searchUsers,
+  fetchDomainMutes,
+  muteDomain,
+  unmuteDomain
 }
 
 export default apiService