forked from AkkomaGang/akkoma-fe
New search
This commit is contained in:
parent
0c06410584
commit
69a4bcb238
17 changed files with 451 additions and 173 deletions
12
src/App.js
12
src/App.js
|
@ -1,7 +1,7 @@
|
||||||
import UserPanel from './components/user_panel/user_panel.vue'
|
import UserPanel from './components/user_panel/user_panel.vue'
|
||||||
import NavPanel from './components/nav_panel/nav_panel.vue'
|
import NavPanel from './components/nav_panel/nav_panel.vue'
|
||||||
import Notifications from './components/notifications/notifications.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 InstanceSpecificPanel from './components/instance_specific_panel/instance_specific_panel.vue'
|
||||||
import FeaturesPanel from './components/features_panel/features_panel.vue'
|
import FeaturesPanel from './components/features_panel/features_panel.vue'
|
||||||
import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_panel.vue'
|
import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_panel.vue'
|
||||||
|
@ -19,7 +19,7 @@ export default {
|
||||||
UserPanel,
|
UserPanel,
|
||||||
NavPanel,
|
NavPanel,
|
||||||
Notifications,
|
Notifications,
|
||||||
UserFinder,
|
SearchBar,
|
||||||
InstanceSpecificPanel,
|
InstanceSpecificPanel,
|
||||||
FeaturesPanel,
|
FeaturesPanel,
|
||||||
WhoToFollowPanel,
|
WhoToFollowPanel,
|
||||||
|
@ -32,7 +32,7 @@ export default {
|
||||||
},
|
},
|
||||||
data: () => ({
|
data: () => ({
|
||||||
mobileActivePanel: 'timeline',
|
mobileActivePanel: 'timeline',
|
||||||
finderHidden: true,
|
searchBarHidden: true,
|
||||||
supportsMask: window.CSS && window.CSS.supports && (
|
supportsMask: window.CSS && window.CSS.supports && (
|
||||||
window.CSS.supports('mask-size', 'contain') ||
|
window.CSS.supports('mask-size', 'contain') ||
|
||||||
window.CSS.supports('-webkit-mask-size', 'contain') ||
|
window.CSS.supports('-webkit-mask-size', 'contain') ||
|
||||||
|
@ -70,7 +70,7 @@ export default {
|
||||||
logoBgStyle () {
|
logoBgStyle () {
|
||||||
return Object.assign({
|
return Object.assign({
|
||||||
'margin': `${this.$store.state.instance.logoMargin} 0`,
|
'margin': `${this.$store.state.instance.logoMargin} 0`,
|
||||||
opacity: this.finderHidden ? 1 : 0
|
opacity: this.searchBarHidden ? 1 : 0
|
||||||
}, this.enableMask ? {} : {
|
}, this.enableMask ? {} : {
|
||||||
'background-color': this.enableMask ? '' : 'transparent'
|
'background-color': this.enableMask ? '' : 'transparent'
|
||||||
})
|
})
|
||||||
|
@ -101,8 +101,8 @@ export default {
|
||||||
this.$router.replace('/main/public')
|
this.$router.replace('/main/public')
|
||||||
this.$store.dispatch('logout')
|
this.$store.dispatch('logout')
|
||||||
},
|
},
|
||||||
onFinderToggled (hidden) {
|
onSearchBarToggled (hidden) {
|
||||||
this.finderHidden = hidden
|
this.searchBarHidden = hidden
|
||||||
},
|
},
|
||||||
updateMobileState () {
|
updateMobileState () {
|
||||||
const mobileLayout = windowWidth() <= 800
|
const mobileLayout = windowWidth() <= 800
|
||||||
|
|
|
@ -38,9 +38,9 @@
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
<div class="item right">
|
<div class="item right">
|
||||||
<user-finder
|
<search-bar
|
||||||
class="button-icon nav-icon mobile-hidden"
|
class="nav-icon mobile-hidden"
|
||||||
@toggled="onFinderToggled"
|
@toggled="onSearchBarToggled"
|
||||||
/>
|
/>
|
||||||
<router-link
|
<router-link
|
||||||
class="mobile-hidden"
|
class="mobile-hidden"
|
||||||
|
|
|
@ -6,12 +6,12 @@ import ConversationPage from 'components/conversation-page/conversation-page.vue
|
||||||
import Interactions from 'components/interactions/interactions.vue'
|
import Interactions from 'components/interactions/interactions.vue'
|
||||||
import DMs from 'components/dm_timeline/dm_timeline.vue'
|
import DMs from 'components/dm_timeline/dm_timeline.vue'
|
||||||
import UserProfile from 'components/user_profile/user_profile.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 Settings from 'components/settings/settings.vue'
|
||||||
import Registration from 'components/registration/registration.vue'
|
import Registration from 'components/registration/registration.vue'
|
||||||
import UserSettings from 'components/user_settings/user_settings.vue'
|
import UserSettings from 'components/user_settings/user_settings.vue'
|
||||||
import FollowRequests from 'components/follow_requests/follow_requests.vue'
|
import FollowRequests from 'components/follow_requests/follow_requests.vue'
|
||||||
import OAuthCallback from 'components/oauth_callback/oauth_callback.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 Notifications from 'components/notifications/notifications.vue'
|
||||||
import AuthForm from 'components/auth_form/auth_form.js'
|
import AuthForm from 'components/auth_form/auth_form.js'
|
||||||
import ChatPanel from 'components/chat_panel/chat_panel.vue'
|
import ChatPanel from 'components/chat_panel/chat_panel.vue'
|
||||||
|
@ -45,7 +45,7 @@ export default (store) => {
|
||||||
{ name: 'login', path: '/login', component: AuthForm },
|
{ name: 'login', path: '/login', component: AuthForm },
|
||||||
{ name: 'chat', path: '/chat', component: ChatPanel, props: () => ({ floating: false }) },
|
{ name: 'chat', path: '/chat', component: ChatPanel, props: () => ({ floating: false }) },
|
||||||
{ name: 'oauth-callback', path: '/oauth-callback', component: OAuthCallback, props: (route) => ({ code: route.query.code }) },
|
{ 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: 'who-to-follow', path: '/who-to-follow', component: WhoToFollow },
|
||||||
{ name: 'about', path: '/about', component: About },
|
{ name: 'about', path: '/about', component: About },
|
||||||
{ name: 'user-profile', path: '/(users/)?:name', component: UserProfile }
|
{ name: 'user-profile', path: '/(users/)?:name', component: UserProfile }
|
||||||
|
|
98
src/components/search/search.js
Normal file
98
src/components/search/search.js
Normal file
|
@ -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
|
211
src/components/search/search.vue
Normal file
211
src/components/search/search.vue
Normal file
|
@ -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>
|
27
src/components/search_bar/search_bar.js
Normal file
27
src/components/search_bar/search_bar.js
Normal file
|
@ -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,36 +1,36 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="user-finder-container">
|
<div class="search-bar-container">
|
||||||
<i
|
<i
|
||||||
v-if="loading"
|
v-if="loading"
|
||||||
class="icon-spin4 user-finder-icon animate-spin-slow"
|
class="icon-spin4 finder-icon animate-spin-slow"
|
||||||
/>
|
/>
|
||||||
<a
|
<a
|
||||||
v-if="hidden"
|
v-if="hidden"
|
||||||
href="#"
|
href="#"
|
||||||
:title="$t('finder.find_user')"
|
:title="$t('nav.search')"
|
||||||
><i
|
><i
|
||||||
class="icon-user-plus user-finder-icon"
|
class="button-icon icon-search"
|
||||||
@click.prevent.stop="toggleHidden"
|
@click.prevent.stop="toggleHidden"
|
||||||
/></a>
|
/></a>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<input
|
<input
|
||||||
id="user-finder-input"
|
id="search-bar-input"
|
||||||
ref="userSearchInput"
|
ref="searchInput"
|
||||||
v-model="username"
|
v-model="searchTerm"
|
||||||
class="user-finder-input"
|
class="search-bar-input"
|
||||||
:placeholder="$t('finder.find_user')"
|
:placeholder="$t('nav.search')"
|
||||||
type="text"
|
type="text"
|
||||||
@keyup.enter="findUser(username)"
|
@keyup.enter="find(searchTerm)"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="btn search-button"
|
class="btn search-button"
|
||||||
@click="findUser(username)"
|
@click="find(searchTerm)"
|
||||||
>
|
>
|
||||||
<i class="icon-search" />
|
<i class="icon-search" />
|
||||||
</button>
|
</button>
|
||||||
<i
|
<i
|
||||||
class="button-icon icon-cancel user-finder-icon"
|
class="button-icon icon-cancel"
|
||||||
@click.prevent.stop="toggleHidden"
|
@click.prevent.stop="toggleHidden"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
@ -38,22 +38,24 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script src="./user_finder.js"></script>
|
<script src="./search_bar.js"></script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import '../../_variables.scss';
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
.user-finder-container {
|
.search-bar-container {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
vertical-align: baseline;
|
vertical-align: baseline;
|
||||||
|
justify-content: flex-end;
|
||||||
|
|
||||||
.user-finder-input,
|
.search-bar-input,
|
||||||
.search-button {
|
.search-button {
|
||||||
height: 29px;
|
height: 29px;
|
||||||
}
|
}
|
||||||
.user-finder-input {
|
|
||||||
|
.search-bar-input {
|
||||||
// TODO: do this properly without a rough guesstimate of 2 icons + paddings
|
// TODO: do this properly without a rough guesstimate of 2 icons + paddings
|
||||||
max-width: calc(100% - 30px - 30px - 20px);
|
max-width: calc(100% - 30px - 30px - 20px);
|
||||||
}
|
}
|
||||||
|
@ -62,6 +64,10 @@
|
||||||
margin-left: .5em;
|
margin-left: .5em;
|
||||||
margin-right: .5em;
|
margin-right: .5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon-cancel {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
|
@ -100,8 +100,8 @@
|
||||||
</ul>
|
</ul>
|
||||||
<ul>
|
<ul>
|
||||||
<li @click="toggleDrawer">
|
<li @click="toggleDrawer">
|
||||||
<router-link :to="{ name: 'user-search' }">
|
<router-link :to="{ name: 'search' }">
|
||||||
{{ $t("nav.user_search") }}
|
{{ $t("nav.search") }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
|
|
|
@ -4,7 +4,7 @@ import './tab_switcher.scss'
|
||||||
|
|
||||||
export default Vue.component('tab-switcher', {
|
export default Vue.component('tab-switcher', {
|
||||||
name: 'TabSwitcher',
|
name: 'TabSwitcher',
|
||||||
props: ['renderOnlyFocused', 'onSwitch'],
|
props: ['renderOnlyFocused', 'onSwitch', 'customActive'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
active: this.$slots.default.findIndex(_ => _.tag)
|
active: this.$slots.default.findIndex(_ => _.tag)
|
||||||
|
@ -24,6 +24,14 @@ export default Vue.component('tab-switcher', {
|
||||||
}
|
}
|
||||||
this.active = index
|
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) {
|
render (h) {
|
||||||
|
@ -33,7 +41,7 @@ export default Vue.component('tab-switcher', {
|
||||||
const classesTab = ['tab']
|
const classesTab = ['tab']
|
||||||
const classesWrapper = ['tab-wrapper']
|
const classesWrapper = ['tab-wrapper']
|
||||||
|
|
||||||
if (index === this.active) {
|
if (this.isActiveTab(index)) {
|
||||||
classesTab.push('active')
|
classesTab.push('active')
|
||||||
classesWrapper.push('active')
|
classesWrapper.push('active')
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,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
|
|
|
@ -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>
|
|
|
@ -78,6 +78,7 @@
|
||||||
"timeline": "Timeline",
|
"timeline": "Timeline",
|
||||||
"twkn": "The Whole Known Network",
|
"twkn": "The Whole Known Network",
|
||||||
"user_search": "User Search",
|
"user_search": "User Search",
|
||||||
|
"search": "Search",
|
||||||
"who_to_follow": "Who to follow",
|
"who_to_follow": "Who to follow",
|
||||||
"preferences": "Preferences"
|
"preferences": "Preferences"
|
||||||
},
|
},
|
||||||
|
@ -595,5 +596,12 @@
|
||||||
"GiB": "GiB",
|
"GiB": "GiB",
|
||||||
"TiB": "TiB"
|
"TiB": "TiB"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"search": {
|
||||||
|
"people": "People",
|
||||||
|
"hashtags": "Hashtags",
|
||||||
|
"person_talking": "{count} person talking",
|
||||||
|
"people_talking": "{count} people talking",
|
||||||
|
"no_results": "No results"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,7 +38,8 @@
|
||||||
"interactions": "Взаимодействия",
|
"interactions": "Взаимодействия",
|
||||||
"public_tl": "Публичная лента",
|
"public_tl": "Публичная лента",
|
||||||
"timeline": "Лента",
|
"timeline": "Лента",
|
||||||
"twkn": "Федеративная лента"
|
"twkn": "Федеративная лента",
|
||||||
|
"search": "Поиск"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"broken_favorite": "Неизвестный статус, ищем...",
|
"broken_favorite": "Неизвестный статус, ищем...",
|
||||||
|
@ -381,5 +382,12 @@
|
||||||
},
|
},
|
||||||
"user_profile": {
|
"user_profile": {
|
||||||
"timeline_title": "Лента пользователя"
|
"timeline_title": "Лента пользователя"
|
||||||
|
},
|
||||||
|
"search": {
|
||||||
|
"people": "Люди",
|
||||||
|
"hashtags": "Хэштэги",
|
||||||
|
"person_talking": "Популярно у {count} человека",
|
||||||
|
"people_talking": "Популярно у {count} человек",
|
||||||
|
"no_results": "Ничего не найдено"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -602,6 +602,14 @@ const statuses = {
|
||||||
fetchRepeats ({ rootState, commit }, id) {
|
fetchRepeats ({ rootState, commit }, id) {
|
||||||
rootState.api.backendInteractor.fetchRebloggedByUsers(id)
|
rootState.api.backendInteractor.fetchRebloggedByUsers(id)
|
||||||
.then(rebloggedByUsers => commit('addRepeats', { id, rebloggedByUsers, currentUser: rootState.users.currentUser }))
|
.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
|
mutations
|
||||||
|
|
|
@ -67,7 +67,7 @@ const MASTODON_PROFILE_UPDATE_URL = '/api/v1/accounts/update_credentials'
|
||||||
const MASTODON_REPORT_USER_URL = '/api/v1/reports'
|
const MASTODON_REPORT_USER_URL = '/api/v1/reports'
|
||||||
const MASTODON_PIN_OWN_STATUS = id => `/api/v1/statuses/${id}/pin`
|
const MASTODON_PIN_OWN_STATUS = id => `/api/v1/statuses/${id}/pin`
|
||||||
const MASTODON_UNPIN_OWN_STATUS = id => `/api/v1/statuses/${id}/unpin`
|
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
|
const oldfetch = window.fetch
|
||||||
|
|
||||||
|
@ -853,16 +853,46 @@ const reportUser = ({ credentials, userId, statusIds, comment, forward }) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const searchUsers = ({ credentials, query }) => {
|
const search2 = ({ credentials, q, resolve, limit, offset, following }) => {
|
||||||
return promisedRequest({
|
let url = MASTODON_SEARCH_2
|
||||||
url: MASTODON_USER_SEARCH_URL,
|
let params = []
|
||||||
params: {
|
|
||||||
q: query,
|
if (q) {
|
||||||
resolve: true
|
params.push(['q', encodeURIComponent(q)])
|
||||||
},
|
}
|
||||||
credentials
|
|
||||||
|
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
|
||||||
})
|
})
|
||||||
.then((data) => data.map(parseUser))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const apiService = {
|
const apiService = {
|
||||||
|
@ -930,7 +960,7 @@ const apiService = {
|
||||||
fetchRebloggedByUsers,
|
fetchRebloggedByUsers,
|
||||||
reportUser,
|
reportUser,
|
||||||
updateNotificationSettings,
|
updateNotificationSettings,
|
||||||
searchUsers
|
search2
|
||||||
}
|
}
|
||||||
|
|
||||||
export default apiService
|
export default apiService
|
||||||
|
|
|
@ -148,8 +148,8 @@ const backendInteractorService = credentials => {
|
||||||
const unfavorite = (id) => apiService.unfavorite({ id, credentials })
|
const unfavorite = (id) => apiService.unfavorite({ id, credentials })
|
||||||
const retweet = (id) => apiService.retweet({ id, credentials })
|
const retweet = (id) => apiService.retweet({ id, credentials })
|
||||||
const unretweet = (id) => apiService.unretweet({ id, credentials })
|
const unretweet = (id) => apiService.unretweet({ id, credentials })
|
||||||
|
const search2 = ({ q, resolve, limit, offset, following }) =>
|
||||||
const searchUsers = (query) => apiService.searchUsers({ query, credentials })
|
apiService.search2({ credentials, q, resolve, limit, offset, following })
|
||||||
|
|
||||||
const backendInteractorServiceInstance = {
|
const backendInteractorServiceInstance = {
|
||||||
fetchStatus,
|
fetchStatus,
|
||||||
|
@ -212,7 +212,7 @@ const backendInteractorService = credentials => {
|
||||||
retweet,
|
retweet,
|
||||||
unretweet,
|
unretweet,
|
||||||
updateNotificationSettings,
|
updateNotificationSettings,
|
||||||
searchUsers
|
search2
|
||||||
}
|
}
|
||||||
|
|
||||||
return backendInteractorServiceInstance
|
return backendInteractorServiceInstance
|
||||||
|
|
Loading…
Reference in a new issue