# Conflicts: # src/services/api/api.service.jsfix/no-extra-buttons
commit
3370dd80dc
@ -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
|
@ -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>
|
@ -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
|
@ -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
|
@ -1,51 +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) {
|
||||
this.users = []
|
||||
return
|
||||
}
|
||||
this.loading = true
|
||||
this.$store.dispatch('searchUsers', query)
|
||||
.then((res) => {
|
||||
this.loading = false
|
||||
this.userIds = map(res, 'id')
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default userSearch
|
@ -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>
|
@ -1,20 +0,0 @@
|
||||
import utils from './utils.js'
|
||||
import { parseUser } from '../entity_normalizer/entity_normalizer.service.js'
|
||||
|
||||
const search = ({ query, store }) => {
|
||||
return utils.request({
|
||||
store,
|
||||
url: '/api/v1/accounts/search',
|
||||
params: {
|
||||
q: query,
|
||||
resolve: true
|
||||
}
|
||||
})
|
||||
.then((data) => data.json())
|
||||
.then((data) => data.map(parseUser))
|
||||
}
|
||||
const UserSearch = {
|
||||
search
|
||||
}
|
||||
|
||||
export default UserSearch
|
@ -1,36 +0,0 @@
|
||||
const queryParams = (params) => {
|
||||
return Object.keys(params)
|
||||
.map(k => encodeURIComponent(k) + '=' + encodeURIComponent(params[k]))
|
||||
.join('&')
|
||||
}
|
||||
|
||||
const headers = (store) => {
|
||||
const accessToken = store.getters.getToken()
|
||||
if (accessToken) {
|
||||
return { 'Authorization': `Bearer ${accessToken}` }
|
||||
} else {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
const request = ({ method = 'GET', url, params, store }) => {
|
||||
const instance = store.state.instance.server
|
||||
let fullUrl = `${instance}${url}`
|
||||
|
||||
if (method === 'GET' && params) {
|
||||
fullUrl = fullUrl + `?${queryParams(params)}`
|
||||
}
|
||||
|
||||
return window.fetch(fullUrl, {
|
||||
method,
|
||||
headers: headers(store),
|
||||
credentials: 'same-origin'
|
||||
})
|
||||
}
|
||||
|
||||
const utils = {
|
||||
queryParams,
|
||||
request
|
||||
}
|
||||
|
||||
export default utils
|
File diff suppressed because one or more lines are too long
Binary file not shown.
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
Reference in new issue