From 69a4bcb238b347a139bfb1192413b45c8b9d7e36 Mon Sep 17 00:00:00 2001
From: Eugenij <eugenijm@protonmail.com>
Date: Mon, 15 Jul 2019 16:42:27 +0000
Subject: [PATCH] New search

---
 src/App.js                                    |  12 +-
 src/App.vue                                   |   6 +-
 src/boot/routes.js                            |   4 +-
 src/components/search/search.js               |  98 ++++++++
 src/components/search/search.vue              | 211 ++++++++++++++++++
 src/components/search_bar/search_bar.js       |  27 +++
 .../search_bar.vue}                           |  38 ++--
 src/components/side_drawer/side_drawer.vue    |   4 +-
 src/components/tab_switcher/tab_switcher.js   |  12 +-
 src/components/user_finder/user_finder.js     |  20 --
 src/components/user_search/user_search.js     |  49 ----
 src/components/user_search/user_search.vue    |  57 -----
 src/i18n/en.json                              |   8 +
 src/i18n/ru.json                              |  10 +-
 src/modules/statuses.js                       |   8 +
 src/services/api/api.service.js               |  54 ++++-
 .../backend_interactor_service.js             |   6 +-
 17 files changed, 451 insertions(+), 173 deletions(-)
 create mode 100644 src/components/search/search.js
 create mode 100644 src/components/search/search.vue
 create mode 100644 src/components/search_bar/search_bar.js
 rename src/components/{user_finder/user_finder.vue => search_bar/search_bar.vue} (58%)
 delete mode 100644 src/components/user_finder/user_finder.js
 delete mode 100644 src/components/user_search/user_search.js
 delete mode 100644 src/components/user_search/user_search.vue

diff --git a/src/App.js b/src/App.js
index e72c73e35..3624171ed 100644
--- a/src/App.js
+++ b/src/App.js
@@ -1,7 +1,7 @@
 import UserPanel from './components/user_panel/user_panel.vue'
 import NavPanel from './components/nav_panel/nav_panel.vue'
 import Notifications from './components/notifications/notifications.vue'
-import UserFinder from './components/user_finder/user_finder.vue'
+import SearchBar from './components/search_bar/search_bar.vue'
 import InstanceSpecificPanel from './components/instance_specific_panel/instance_specific_panel.vue'
 import FeaturesPanel from './components/features_panel/features_panel.vue'
 import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_panel.vue'
@@ -19,7 +19,7 @@ export default {
     UserPanel,
     NavPanel,
     Notifications,
-    UserFinder,
+    SearchBar,
     InstanceSpecificPanel,
     FeaturesPanel,
     WhoToFollowPanel,
@@ -32,7 +32,7 @@ export default {
   },
   data: () => ({
     mobileActivePanel: 'timeline',
-    finderHidden: true,
+    searchBarHidden: true,
     supportsMask: window.CSS && window.CSS.supports && (
       window.CSS.supports('mask-size', 'contain') ||
         window.CSS.supports('-webkit-mask-size', 'contain') ||
@@ -70,7 +70,7 @@ export default {
     logoBgStyle () {
       return Object.assign({
         'margin': `${this.$store.state.instance.logoMargin} 0`,
-        opacity: this.finderHidden ? 1 : 0
+        opacity: this.searchBarHidden ? 1 : 0
       }, this.enableMask ? {} : {
         'background-color': this.enableMask ? '' : 'transparent'
       })
@@ -101,8 +101,8 @@ export default {
       this.$router.replace('/main/public')
       this.$store.dispatch('logout')
     },
-    onFinderToggled (hidden) {
-      this.finderHidden = hidden
+    onSearchBarToggled (hidden) {
+      this.searchBarHidden = hidden
     },
     updateMobileState () {
       const mobileLayout = windowWidth() <= 800
diff --git a/src/App.vue b/src/App.vue
index 758c9fce1..be4d1f754 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -38,9 +38,9 @@
           </router-link>
         </div>
         <div class="item right">
-          <user-finder
-            class="button-icon nav-icon mobile-hidden"
-            @toggled="onFinderToggled"
+          <search-bar
+            class="nav-icon mobile-hidden"
+            @toggled="onSearchBarToggled"
           />
           <router-link
             class="mobile-hidden"
diff --git a/src/boot/routes.js b/src/boot/routes.js
index ca4a6a3ee..22641f833 100644
--- a/src/boot/routes.js
+++ b/src/boot/routes.js
@@ -6,12 +6,12 @@ import ConversationPage from 'components/conversation-page/conversation-page.vue
 import Interactions from 'components/interactions/interactions.vue'
 import DMs from 'components/dm_timeline/dm_timeline.vue'
 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 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'
-import UserSearch from 'components/user_search/user_search.vue'
 import Notifications from 'components/notifications/notifications.vue'
 import AuthForm from 'components/auth_form/auth_form.js'
 import ChatPanel from 'components/chat_panel/chat_panel.vue'
@@ -45,7 +45,7 @@ export default (store) => {
     { name: 'login', path: '/login', component: AuthForm },
     { name: 'chat', path: '/chat', component: ChatPanel, props: () => ({ floating: false }) },
     { name: 'oauth-callback', path: '/oauth-callback', component: OAuthCallback, props: (route) => ({ code: route.query.code }) },
-    { name: 'user-search', path: '/user-search', component: UserSearch, props: (route) => ({ query: route.query.query }) },
+    { name: 'search', path: '/search', component: Search, props: (route) => ({ query: route.query.query }) },
     { name: 'who-to-follow', path: '/who-to-follow', component: WhoToFollow },
     { name: 'about', path: '/about', component: About },
     { name: 'user-profile', path: '/(users/)?:name', component: UserProfile }
diff --git a/src/components/search/search.js b/src/components/search/search.js
new file mode 100644
index 000000000..b434e1272
--- /dev/null
+++ b/src/components/search/search.js
@@ -0,0 +1,98 @@
+import FollowCard from '../follow_card/follow_card.vue'
+import Conversation from '../conversation/conversation.vue'
+import Status from '../status/status.vue'
+import map from 'lodash/map'
+
+const Search = {
+  components: {
+    FollowCard,
+    Conversation,
+    Status
+  },
+  props: [
+    'query'
+  ],
+  data () {
+    return {
+      loaded: false,
+      loading: false,
+      searchTerm: this.query || '',
+      userIds: [],
+      statuses: [],
+      hashtags: [],
+      currenResultTab: 'statuses'
+    }
+  },
+  computed: {
+    users () {
+      return this.userIds.map(userId => this.$store.getters.findUser(userId))
+    },
+    visibleStatuses () {
+      const allStatusesObject = this.$store.state.statuses.allStatusesObject
+
+      return this.statuses.filter(status =>
+        allStatusesObject[status.id] && !allStatusesObject[status.id].deleted
+      )
+    }
+  },
+  mounted () {
+    this.search(this.query)
+  },
+  watch: {
+    query (newValue) {
+      this.searchTerm = newValue
+      this.search(newValue)
+    }
+  },
+  methods: {
+    newQuery (query) {
+      this.$router.push({ name: 'search', query: { query } })
+      this.$refs.searchInput.focus()
+    },
+    search (query) {
+      if (!query) {
+        this.loading = false
+        return
+      }
+
+      this.loading = true
+      this.userIds = []
+      this.statuses = []
+      this.hashtags = []
+      this.$refs.searchInput.blur()
+
+      this.$store.dispatch('search', { q: query, resolve: true })
+        .then(data => {
+          this.loading = false
+          this.userIds = map(data.accounts, 'id')
+          this.statuses = data.statuses
+          this.hashtags = data.hashtags
+          this.currenResultTab = this.getActiveTab()
+          this.loaded = true
+        })
+    },
+    resultCount (tabName) {
+      const length = this[tabName].length
+      return length === 0 ? '' : ` (${length})`
+    },
+    onResultTabSwitch (_index, dataset) {
+      this.currenResultTab = dataset.filter
+    },
+    getActiveTab () {
+      if (this.visibleStatuses.length > 0) {
+        return 'statuses'
+      } else if (this.users.length > 0) {
+        return 'people'
+      } else if (this.hashtags.length > 0) {
+        return 'hashtags'
+      }
+
+      return 'statuses'
+    },
+    lastHistoryRecord (hashtag) {
+      return hashtag.history && hashtag.history[0]
+    }
+  }
+}
+
+export default Search
diff --git a/src/components/search/search.vue b/src/components/search/search.vue
new file mode 100644
index 000000000..4350e672d
--- /dev/null
+++ b/src/components/search/search.vue
@@ -0,0 +1,211 @@
+<template>
+  <div class="panel panel-default">
+    <div class="panel-heading">
+      <div class="title">
+        {{ $t('nav.search') }}
+      </div>
+    </div>
+    <div class="search-input-container">
+      <input
+        ref="searchInput"
+        v-model="searchTerm"
+        class="search-input"
+        :placeholder="$t('nav.search')"
+        @keyup.enter="newQuery(searchTerm)"
+      >
+      <button
+        class="btn search-button"
+        @click="newQuery(searchTerm)"
+      >
+        <i class="icon-search" />
+      </button>
+    </div>
+    <div
+      v-if="loading"
+      class="text-center loading-icon"
+    >
+      <i class="icon-spin3 animate-spin" />
+    </div>
+    <div v-else-if="loaded">
+      <div class="search-nav-heading">
+        <tab-switcher
+          ref="tabSwitcher"
+          :on-switch="onResultTabSwitch"
+          :custom-active="currenResultTab"
+        >
+          <span
+            data-tab-dummy
+            data-filter="statuses"
+            :label="$t('user_card.statuses') + resultCount('visibleStatuses')"
+          />
+          <span
+            data-tab-dummy
+            data-filter="people"
+            :label="$t('search.people') + resultCount('users')"
+          />
+          <span
+            data-tab-dummy
+            data-filter="hashtags"
+            :label="$t('search.hashtags') + resultCount('hashtags')"
+          />
+        </tab-switcher>
+      </div>
+    </div>
+    <div class="panel-body">
+      <div v-if="currenResultTab === 'statuses'">
+        <div
+          v-if="visibleStatuses.length === 0 && !loading && loaded"
+          class="search-result-heading"
+        >
+          <h4>{{ $t('search.no_results') }}</h4>
+        </div>
+        <Status
+          v-for="status in visibleStatuses"
+          :key="status.id"
+          :collapsable="false"
+          :expandable="false"
+          :compact="false"
+          class="search-result"
+          :statusoid="status"
+          :no-heading="false"
+        />
+      </div>
+      <div v-else-if="currenResultTab === 'people'">
+        <div
+          v-if="users.length === 0 && !loading && loaded"
+          class="search-result-heading"
+        >
+          <h4>{{ $t('search.no_results') }}</h4>
+        </div>
+        <FollowCard
+          v-for="user in users"
+          :key="user.id"
+          :user="user"
+          class="list-item search-result"
+        />
+      </div>
+      <div v-else-if="currenResultTab === 'hashtags'">
+        <div
+          v-if="hashtags.length === 0 && !loading && loaded"
+          class="search-result-heading"
+        >
+          <h4>{{ $t('search.no_results') }}</h4>
+        </div>
+        <div
+          v-for="hashtag in hashtags"
+          :key="hashtag.url"
+          class="status trend search-result"
+        >
+          <div class="hashtag">
+            <router-link :to="{ name: 'tag-timeline', params: { tag: hashtag.name } }">
+              #{{ hashtag.name }}
+            </router-link>
+            <div v-if="lastHistoryRecord(hashtag)">
+              <span v-if="lastHistoryRecord(hashtag).accounts == 1">
+                {{ $t('search.person_talking', { count: lastHistoryRecord(hashtag).accounts }) }}
+              </span>
+              <span v-else>
+                {{ $t('search.people_talking', { count: lastHistoryRecord(hashtag).accounts }) }}
+              </span>
+            </div>
+          </div>
+          <div
+            v-if="lastHistoryRecord(hashtag)"
+            class="count"
+          >
+            {{ lastHistoryRecord(hashtag).uses }}
+          </div>
+        </div>
+      </div>
+    </div>
+    <div class="search-result-footer text-center panel-footer faint" />
+  </div>
+</template>
+
+<script src="./search.js"></script>
+
+<style lang="scss">
+@import '../../_variables.scss';
+
+.search-result-heading {
+  color: $fallback--faint;
+  color: var(--faint, $fallback--faint);
+  padding: 0.75rem;
+  text-align: center;
+}
+
+@media all and (max-width: 800px) {
+  .search-nav-heading {
+    .tab-switcher .tabs .tab-wrapper {
+      display: block;
+      justify-content: center;
+      flex: 1 1 auto;
+      text-align: center;
+    }
+  }
+}
+
+.search-result {
+  box-sizing: border-box;
+  border-bottom: 1px solid;
+  border-color: $fallback--border;
+  border-color: var(--border, $fallback--border);
+}
+
+.search-result-footer {
+  border-width: 1px 0 0 0;
+  border-style: solid;
+  border-color: var(--border, $fallback--border);
+  padding: 10px;
+  background-color: $fallback--fg;
+  background-color: var(--panel, $fallback--fg);
+}
+
+.search-input-container {
+  padding: 0.8rem;
+  display: flex;
+  justify-content: center;
+
+  .search-input {
+    width: 100%;
+    line-height: 1.125rem;
+    font-size: 1rem;
+    padding: 0.5rem;
+    box-sizing: border-box;
+  }
+
+  .search-button {
+    margin-left: 0.5em;
+  }
+}
+
+.loading-icon {
+  padding: 1em;
+}
+
+.trend {
+  display: flex;
+  align-items: center;
+
+  .hashtag {
+    flex: 1 1 auto;
+    color: $fallback--text;
+    color: var(--text, $fallback--text);
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+  }
+
+  .count {
+    flex: 0 0 auto;
+    width: 2rem;
+    font-size: 1.5rem;
+    line-height: 2.25rem;
+    font-weight: 500;
+    text-align: center;
+    color: $fallback--text;
+    color: var(--text, $fallback--text);
+  }
+}
+
+</style>
diff --git a/src/components/search_bar/search_bar.js b/src/components/search_bar/search_bar.js
new file mode 100644
index 000000000..b8a792eef
--- /dev/null
+++ b/src/components/search_bar/search_bar.js
@@ -0,0 +1,27 @@
+const SearchBar = {
+  data: () => ({
+    searchTerm: undefined,
+    hidden: true,
+    error: false,
+    loading: false
+  }),
+  watch: {
+    '$route': function (route) {
+      if (route.name === 'search') {
+        this.searchTerm = route.query.query
+      }
+    }
+  },
+  methods: {
+    find (searchTerm) {
+      this.$router.push({ name: 'search', query: { query: searchTerm } })
+      this.$refs.searchInput.focus()
+    },
+    toggleHidden () {
+      this.hidden = !this.hidden
+      this.$emit('toggled', this.hidden)
+    }
+  }
+}
+
+export default SearchBar
diff --git a/src/components/user_finder/user_finder.vue b/src/components/search_bar/search_bar.vue
similarity index 58%
rename from src/components/user_finder/user_finder.vue
rename to src/components/search_bar/search_bar.vue
index 39d492374..4d5a1aec0 100644
--- a/src/components/user_finder/user_finder.vue
+++ b/src/components/search_bar/search_bar.vue
@@ -1,36 +1,36 @@
 <template>
   <div>
-    <div class="user-finder-container">
+    <div class="search-bar-container">
       <i
         v-if="loading"
-        class="icon-spin4 user-finder-icon animate-spin-slow"
+        class="icon-spin4 finder-icon animate-spin-slow"
       />
       <a
         v-if="hidden"
         href="#"
-        :title="$t('finder.find_user')"
+        :title="$t('nav.search')"
       ><i
-        class="icon-user-plus user-finder-icon"
+        class="button-icon icon-search"
         @click.prevent.stop="toggleHidden"
       /></a>
       <template v-else>
         <input
-          id="user-finder-input"
-          ref="userSearchInput"
-          v-model="username"
-          class="user-finder-input"
-          :placeholder="$t('finder.find_user')"
+          id="search-bar-input"
+          ref="searchInput"
+          v-model="searchTerm"
+          class="search-bar-input"
+          :placeholder="$t('nav.search')"
           type="text"
-          @keyup.enter="findUser(username)"
+          @keyup.enter="find(searchTerm)"
         >
         <button
           class="btn search-button"
-          @click="findUser(username)"
+          @click="find(searchTerm)"
         >
           <i class="icon-search" />
         </button>
         <i
-          class="button-icon icon-cancel user-finder-icon"
+          class="button-icon icon-cancel"
           @click.prevent.stop="toggleHidden"
         />
       </template>
@@ -38,22 +38,24 @@
   </div>
 </template>
 
-<script src="./user_finder.js"></script>
+<script src="./search_bar.js"></script>
 
 <style lang="scss">
 @import '../../_variables.scss';
 
-.user-finder-container {
+.search-bar-container {
   max-width: 100%;
   display: inline-flex;
   align-items: baseline;
   vertical-align: baseline;
+  justify-content: flex-end;
 
-  .user-finder-input,
+  .search-bar-input,
   .search-button {
     height: 29px;
   }
-  .user-finder-input {
+
+  .search-bar-input {
     // TODO: do this properly without a rough guesstimate of 2 icons + paddings
     max-width: calc(100% - 30px - 30px - 20px);
   }
@@ -62,6 +64,10 @@
     margin-left: .5em;
     margin-right: .5em;
   }
+
+  .icon-cancel {
+    cursor: pointer;
+  }
 }
 
 </style>
diff --git a/src/components/side_drawer/side_drawer.vue b/src/components/side_drawer/side_drawer.vue
index 80b75ce57..5b2d44732 100644
--- a/src/components/side_drawer/side_drawer.vue
+++ b/src/components/side_drawer/side_drawer.vue
@@ -100,8 +100,8 @@
       </ul>
       <ul>
         <li @click="toggleDrawer">
-          <router-link :to="{ name: 'user-search' }">
-            {{ $t("nav.user_search") }}
+          <router-link :to="{ name: 'search' }">
+            {{ $t("nav.search") }}
           </router-link>
         </li>
         <li
diff --git a/src/components/tab_switcher/tab_switcher.js b/src/components/tab_switcher/tab_switcher.js
index 81e4d333f..67835231b 100644
--- a/src/components/tab_switcher/tab_switcher.js
+++ b/src/components/tab_switcher/tab_switcher.js
@@ -4,7 +4,7 @@ import './tab_switcher.scss'
 
 export default Vue.component('tab-switcher', {
   name: 'TabSwitcher',
-  props: ['renderOnlyFocused', 'onSwitch'],
+  props: ['renderOnlyFocused', 'onSwitch', 'customActive'],
   data () {
     return {
       active: this.$slots.default.findIndex(_ => _.tag)
@@ -24,6 +24,14 @@ export default Vue.component('tab-switcher', {
         }
         this.active = index
       }
+    },
+    isActiveTab (index) {
+      const customActiveIndex = this.$slots.default.findIndex(slot => {
+        const dataFilter = slot.data && slot.data.attrs && slot.data.attrs['data-filter']
+        return this.customActive && this.customActive === dataFilter
+      })
+
+      return customActiveIndex > -1 ? customActiveIndex === index : index === this.active
     }
   },
   render (h) {
@@ -33,7 +41,7 @@ export default Vue.component('tab-switcher', {
         const classesTab = ['tab']
         const classesWrapper = ['tab-wrapper']
 
-        if (index === this.active) {
+        if (this.isActiveTab(index)) {
           classesTab.push('active')
           classesWrapper.push('active')
         }
diff --git a/src/components/user_finder/user_finder.js b/src/components/user_finder/user_finder.js
deleted file mode 100644
index 27153f45f..000000000
--- a/src/components/user_finder/user_finder.js
+++ /dev/null
@@ -1,20 +0,0 @@
-const UserFinder = {
-  data: () => ({
-    username: undefined,
-    hidden: true,
-    error: false,
-    loading: false
-  }),
-  methods: {
-    findUser (username) {
-      this.$router.push({ name: 'user-search', query: { query: username } })
-      this.$refs.userSearchInput.focus()
-    },
-    toggleHidden () {
-      this.hidden = !this.hidden
-      this.$emit('toggled', this.hidden)
-    }
-  }
-}
-
-export default UserFinder
diff --git a/src/components/user_search/user_search.js b/src/components/user_search/user_search.js
deleted file mode 100644
index 5c29d8f2d..000000000
--- a/src/components/user_search/user_search.js
+++ /dev/null
@@ -1,49 +0,0 @@
-import FollowCard from '../follow_card/follow_card.vue'
-import map from 'lodash/map'
-
-const userSearch = {
-  components: {
-    FollowCard
-  },
-  props: [
-    'query'
-  ],
-  data () {
-    return {
-      username: '',
-      userIds: [],
-      loading: false
-    }
-  },
-  computed: {
-    users () {
-      return this.userIds.map(userId => this.$store.getters.findUser(userId))
-    }
-  },
-  mounted () {
-    this.search(this.query)
-  },
-  watch: {
-    query (newV) {
-      this.search(newV)
-    }
-  },
-  methods: {
-    newQuery (query) {
-      this.$router.push({ name: 'user-search', query: { query } })
-      this.$refs.userSearchInput.focus()
-    },
-    search (query) {
-      if (!query) {
-        return
-      }
-      this.loading = true
-      this.userIds = []
-      this.$store.dispatch('searchUsers', query)
-        .then((res) => { this.userIds = map(res, 'id') })
-        .finally(() => { this.loading = false })
-    }
-  }
-}
-
-export default userSearch
diff --git a/src/components/user_search/user_search.vue b/src/components/user_search/user_search.vue
deleted file mode 100644
index e1c6074c8..000000000
--- a/src/components/user_search/user_search.vue
+++ /dev/null
@@ -1,57 +0,0 @@
-<template>
-  <div class="user-search panel panel-default">
-    <div class="panel-heading">
-      {{ $t('nav.user_search') }}
-    </div>
-    <div class="user-search-input-container">
-      <input
-        ref="userSearchInput"
-        v-model="username"
-        class="user-finder-input"
-        :placeholder="$t('finder.find_user')"
-        @keyup.enter="newQuery(username)"
-      >
-      <button
-        class="btn search-button"
-        @click="newQuery(username)"
-      >
-        <i class="icon-search" />
-      </button>
-    </div>
-    <div
-      v-if="loading"
-      class="text-center loading-icon"
-    >
-      <i class="icon-spin3 animate-spin" />
-    </div>
-    <div
-      v-else
-      class="panel-body"
-    >
-      <FollowCard
-        v-for="user in users"
-        :key="user.id"
-        :user="user"
-        class="list-item"
-      />
-    </div>
-  </div>
-</template>
-
-<script src="./user_search.js"></script>
-
-<style lang="scss">
-.user-search-input-container {
-  margin: 0.5em;
-  display: flex;
-  justify-content: center;
-
-  .search-button {
-    margin-left: 0.5em;
-  }
-}
-
-.loading-icon {
-  padding: 1em;
-}
-</style>
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 49989f78b..b5c6b1d84 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -78,6 +78,7 @@
     "timeline": "Timeline",
     "twkn": "The Whole Known Network",
     "user_search": "User Search",
+    "search": "Search",
     "who_to_follow": "Who to follow",
     "preferences": "Preferences"
   },
@@ -595,5 +596,12 @@
       "GiB": "GiB",
       "TiB": "TiB"
     }
+  },
+  "search": {
+    "people": "People",
+    "hashtags": "Hashtags",
+    "person_talking": "{count} person talking",
+    "people_talking": "{count} people talking",
+    "no_results": "No results"
   }
 }
diff --git a/src/i18n/ru.json b/src/i18n/ru.json
index d24ef0cb7..90ed66643 100644
--- a/src/i18n/ru.json
+++ b/src/i18n/ru.json
@@ -38,7 +38,8 @@
     "interactions": "Взаимодействия",
     "public_tl": "Публичная лента",
     "timeline": "Лента",
-    "twkn": "Федеративная лента"
+    "twkn": "Федеративная лента",
+    "search": "Поиск"
   },
   "notifications": {
     "broken_favorite": "Неизвестный статус, ищем...",
@@ -381,5 +382,12 @@
   },
   "user_profile": {
     "timeline_title": "Лента пользователя"
+  },
+  "search": {
+    "people": "Люди",
+    "hashtags": "Хэштэги",
+    "person_talking": "Популярно у {count} человека",
+    "people_talking": "Популярно у {count} человек",
+    "no_results": "Ничего не найдено"
   }
 }
diff --git a/src/modules/statuses.js b/src/modules/statuses.js
index 9d8d768cf..7d5d5a67e 100644
--- a/src/modules/statuses.js
+++ b/src/modules/statuses.js
@@ -602,6 +602,14 @@ const statuses = {
     fetchRepeats ({ rootState, commit }, id) {
       rootState.api.backendInteractor.fetchRebloggedByUsers(id)
         .then(rebloggedByUsers => commit('addRepeats', { id, rebloggedByUsers, currentUser: rootState.users.currentUser }))
+    },
+    search (store, { q, resolve, limit, offset, following }) {
+      return store.rootState.api.backendInteractor.search2({ q, resolve, limit, offset, following })
+        .then((data) => {
+          store.commit('addNewUsers', data.accounts)
+          store.commit('addNewStatuses', { statuses: data.statuses })
+          return data
+        })
     }
   },
   mutations
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index e417cf29c..2de1c3b7a 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -67,7 +67,7 @@ const MASTODON_PROFILE_UPDATE_URL = '/api/v1/accounts/update_credentials'
 const MASTODON_REPORT_USER_URL = '/api/v1/reports'
 const MASTODON_PIN_OWN_STATUS = id => `/api/v1/statuses/${id}/pin`
 const MASTODON_UNPIN_OWN_STATUS = id => `/api/v1/statuses/${id}/unpin`
-const MASTODON_USER_SEARCH_URL = '/api/v1/accounts/search'
+const MASTODON_SEARCH_2 = `/api/v2/search`
 
 const oldfetch = window.fetch
 
@@ -853,16 +853,46 @@ const reportUser = ({ credentials, userId, statusIds, comment, forward }) => {
   })
 }
 
-const searchUsers = ({ credentials, query }) => {
-  return promisedRequest({
-    url: MASTODON_USER_SEARCH_URL,
-    params: {
-      q: query,
-      resolve: true
-    },
-    credentials
-  })
-    .then((data) => data.map(parseUser))
+const search2 = ({ credentials, q, resolve, limit, offset, following }) => {
+  let url = MASTODON_SEARCH_2
+  let params = []
+
+  if (q) {
+    params.push(['q', encodeURIComponent(q)])
+  }
+
+  if (resolve) {
+    params.push(['resolve', resolve])
+  }
+
+  if (limit) {
+    params.push(['limit', limit])
+  }
+
+  if (offset) {
+    params.push(['offset', offset])
+  }
+
+  if (following) {
+    params.push(['following', true])
+  }
+
+  let queryString = map(params, (param) => `${param[0]}=${param[1]}`).join('&')
+  url += `?${queryString}`
+
+  return fetch(url, { headers: authHeaders(credentials) })
+    .then((data) => {
+      if (data.ok) {
+        return data
+      }
+      throw new Error('Error fetching search result', data)
+    })
+    .then((data) => { return data.json() })
+    .then((data) => {
+      data.accounts = data.accounts.slice(0, limit).map(u => parseUser(u))
+      data.statuses = data.statuses.slice(0, limit).map(s => parseStatus(s))
+      return data
+    })
 }
 
 const apiService = {
@@ -930,7 +960,7 @@ const apiService = {
   fetchRebloggedByUsers,
   reportUser,
   updateNotificationSettings,
-  searchUsers
+  search2
 }
 
 export default apiService
diff --git a/src/services/backend_interactor_service/backend_interactor_service.js b/src/services/backend_interactor_service/backend_interactor_service.js
index 4e1675c2e..4f067df98 100644
--- a/src/services/backend_interactor_service/backend_interactor_service.js
+++ b/src/services/backend_interactor_service/backend_interactor_service.js
@@ -148,8 +148,8 @@ const backendInteractorService = credentials => {
   const unfavorite = (id) => apiService.unfavorite({ id, credentials })
   const retweet = (id) => apiService.retweet({ id, credentials })
   const unretweet = (id) => apiService.unretweet({ id, credentials })
-
-  const searchUsers = (query) => apiService.searchUsers({ query, credentials })
+  const search2 = ({ q, resolve, limit, offset, following }) =>
+    apiService.search2({ credentials, q, resolve, limit, offset, following })
 
   const backendInteractorServiceInstance = {
     fetchStatus,
@@ -212,7 +212,7 @@ const backendInteractorService = credentials => {
     retweet,
     unretweet,
     updateNotificationSettings,
-    searchUsers
+    search2
   }
 
   return backendInteractorServiceInstance
-- 
GitLab