Improved search experience
fixed invalid variable calls changed to manage loading state tab by tab made automatic tab selection less aggressive made the api call fail-safe fixed the format of the search result count
This commit is contained in:
parent
bdc1e9d9c6
commit
e8b653737c
2 changed files with 126 additions and 109 deletions
|
@ -15,6 +15,8 @@ library.add(
|
||||||
faSearch
|
faSearch
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const allSearchTypes = Object.freeze(['statuses', 'media', 'accounts', 'hashtags'])
|
||||||
|
|
||||||
const Search = {
|
const Search = {
|
||||||
components: {
|
components: {
|
||||||
FollowCard,
|
FollowCard,
|
||||||
|
@ -28,14 +30,17 @@ const Search = {
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
loadedInitially: false,
|
loadedInitially: false,
|
||||||
loading: false,
|
loading: Object.fromEntries(
|
||||||
|
allSearchTypes.map((searchType) => [searchType, false])
|
||||||
|
),
|
||||||
searchTerm: this.query || '',
|
searchTerm: this.query || '',
|
||||||
userIds: [],
|
userIds: [],
|
||||||
statuses: [],
|
statuses: [],
|
||||||
media: [],
|
media: [],
|
||||||
hashtags: [],
|
hashtags: [],
|
||||||
|
allTabs: allSearchTypes,
|
||||||
currentResultTab: 'statuses',
|
currentResultTab: 'statuses',
|
||||||
preferredTab : 'statuses',
|
hasUserSelectedTab: false,
|
||||||
|
|
||||||
statusesOffset: 0,
|
statusesOffset: 0,
|
||||||
lastStatusFetchCount: 0,
|
lastStatusFetchCount: 0,
|
||||||
|
@ -72,6 +77,10 @@ const Search = {
|
||||||
canSearchFollowing () {
|
canSearchFollowing () {
|
||||||
return this.isLoggedIn &&
|
return this.isLoggedIn &&
|
||||||
this.$store.state.instance.searchOptionFollowingEnabled === true
|
this.$store.state.instance.searchOptionFollowingEnabled === true
|
||||||
|
},
|
||||||
|
hasAtLeastOneResult () {
|
||||||
|
return allSearchTypes
|
||||||
|
.some((searchType) => this.getVisibleLength(searchType) > 0)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
|
@ -97,12 +106,13 @@ const Search = {
|
||||||
},
|
},
|
||||||
async search (query, searchType = null) {
|
async search (query, searchType = null) {
|
||||||
if (!query) {
|
if (!query) {
|
||||||
this.loading = false
|
for (const searchType of allSearchTypes) {
|
||||||
|
this.loading[searchType] = false
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const isNewSearch = this.lastQuery !== query
|
const isNewSearch = this.lastQuery !== query
|
||||||
this.loading = true
|
|
||||||
this.$refs.searchInput.blur()
|
this.$refs.searchInput.blur()
|
||||||
if (isNewSearch) {
|
if (isNewSearch) {
|
||||||
this.userIds = []
|
this.userIds = []
|
||||||
|
@ -116,92 +126,102 @@ const Search = {
|
||||||
this.lastMediaFetchCount = 0
|
this.lastMediaFetchCount = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
let searchTypes = ['statuses', 'media', 'accounts', 'hashtags']
|
let searchTypes = allSearchTypes
|
||||||
|
|
||||||
if (searchType) {
|
if (searchType) {
|
||||||
searchTypes = [searchType]
|
searchTypes = [searchType]
|
||||||
} else if (this.preferredTab !== 'statuses') {
|
} else if (this.currentResultTab !== 'statuses') {
|
||||||
|
// Sort search order; selected tab first
|
||||||
searchTypes = [
|
searchTypes = [
|
||||||
this.preferredTab,
|
this.currentResultTab,
|
||||||
...searchTypes.filter((tab) => tab !== this.preferredTab)
|
...allSearchTypes.filter((tab) => tab !== this.currentResultTab)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const searchType of searchTypes) {
|
||||||
|
this.loading[searchType] = true
|
||||||
|
}
|
||||||
|
|
||||||
let oldStatusesLength = this.statuses.length
|
let oldStatusesLength = this.statuses.length
|
||||||
let oldMediaLength = this.media.length
|
let oldMediaLength = this.media.length
|
||||||
|
|
||||||
let skipMediaSearch = !this.canSearchMediaPosts
|
let skipMediaSearch = !this.canSearchMediaPosts
|
||||||
|
|
||||||
for (const searchType of searchTypes) {
|
for (const searchType of searchTypes) {
|
||||||
if (searchType === 'media' && skipMediaSearch) {
|
try {
|
||||||
continue
|
if (searchType === 'media' && skipMediaSearch) {
|
||||||
}
|
continue
|
||||||
|
|
||||||
let searchOffset
|
|
||||||
if (searchType === 'statuses') {
|
|
||||||
searchOffset = this.statusesOffset
|
|
||||||
} else if (searchType === 'media') {
|
|
||||||
searchOffset = this.mediaOffset
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await this.$store.dispatch('search', {
|
|
||||||
q: query,
|
|
||||||
resolve: true,
|
|
||||||
offset: searchOffset,
|
|
||||||
'type': searchType,
|
|
||||||
following:
|
|
||||||
'followingOnly' in this.filter && this.filter.followingOnly
|
|
||||||
})
|
|
||||||
|
|
||||||
// Always append to old results. If new results are empty, this doesn't change anything
|
|
||||||
this.userIds = this.userIds.concat(map(data.accounts, 'id'))
|
|
||||||
this.statuses = uniqBy(this.statuses.concat(data.statuses), 'id')
|
|
||||||
if ('media' in data) {
|
|
||||||
this.media = uniqBy(this.media.concat(data.media), 'id')
|
|
||||||
}
|
|
||||||
this.hashtags = this.hashtags.concat(data.hashtags)
|
|
||||||
|
|
||||||
if (isNewSearch) {
|
|
||||||
this.currentResultTab = this.getActiveTab()
|
|
||||||
if (searchType === 'statuses' && data.statuses.length === 0) {
|
|
||||||
// safe to assume that there are no media posts
|
|
||||||
skipMediaSearch = true
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
let searchOffset
|
||||||
!loadedInitially &&
|
if (searchType === 'statuses') {
|
||||||
Object.values(data).some((result) => result.length > 0)
|
searchOffset = this.statusesOffset
|
||||||
) {
|
} else if (searchType === 'media') {
|
||||||
// Show results on the first meaningful response
|
searchOffset = this.mediaOffset
|
||||||
this.loadedInitially = true
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (searchType === 'statuses') {
|
const data = await this.$store.dispatch('search', {
|
||||||
// Offset from whatever we already have
|
q: query,
|
||||||
this.statusesOffset = this.statuses.length
|
resolve: true,
|
||||||
// Because the amount of new statuses can actually be zero, compare to old length instead
|
offset: searchOffset,
|
||||||
this.lastStatusFetchCount = this.statuses.length - oldStatusesLength
|
'type': searchType,
|
||||||
} else if (searchType === 'media') {
|
following:
|
||||||
this.mediaOffset = this.media.length
|
'followingOnly' in this.filter && this.filter.followingOnly
|
||||||
this.lastMediaFetchCount = this.media.length - oldMediaLength
|
})
|
||||||
|
|
||||||
|
// Always append to old results. If new results are empty, this doesn't change anything
|
||||||
|
this.userIds = this.userIds.concat(map(data.accounts, 'id'))
|
||||||
|
this.statuses = uniqBy(this.statuses.concat(data.statuses), 'id')
|
||||||
|
if ('media' in data) {
|
||||||
|
this.media = uniqBy(this.media.concat(data.media), 'id')
|
||||||
|
}
|
||||||
|
this.hashtags = this.hashtags.concat(data.hashtags)
|
||||||
|
|
||||||
|
if (isNewSearch) {
|
||||||
|
if (!this.hasUserSelectedTab) {
|
||||||
|
this.currentResultTab = this.getFirstTabWithResults()
|
||||||
|
}
|
||||||
|
if (searchType === 'statuses' && data.statuses.length === 0) {
|
||||||
|
// Safe to assume that there are no media posts
|
||||||
|
skipMediaSearch = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
} finally {
|
||||||
|
if (!this.loadedInitially && this.hasAtLeastOneResult) {
|
||||||
|
// Show results on the first meaningful response
|
||||||
|
this.loadedInitially = true
|
||||||
|
}
|
||||||
|
this.loading[searchType] = false
|
||||||
|
|
||||||
|
if (searchType === 'statuses') {
|
||||||
|
// Offset from whatever we already have
|
||||||
|
this.statusesOffset = this.statuses.length
|
||||||
|
// Because the amount of new statuses can actually be zero, compare to old length instead
|
||||||
|
this.lastStatusFetchCount = this.statuses.length - oldStatusesLength
|
||||||
|
} else if (searchType === 'media') {
|
||||||
|
this.mediaOffset = this.media.length
|
||||||
|
this.lastMediaFetchCount = this.media.length - oldMediaLength
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.lastQuery = query
|
this.lastQuery = query
|
||||||
this.loadedInitially = true
|
this.loadedInitially = true
|
||||||
this.loading = false
|
for (const searchType of allSearchTypes) {
|
||||||
|
this.loading[searchType] = false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
resultCount (tabName) {
|
resultCount (tab) {
|
||||||
const length = this[tabName].length
|
const length = this.getVisibleLength(tab)
|
||||||
|
|
||||||
if (length === 0 || !this.loadedInitially) {
|
if (length === 0 || !this.loadedInitially) {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(tabName === 'visibleStatuses' && this.lastStatusFetchCount !== 0) ||
|
(tab === 'statuses' && this.lastStatusFetchCount !== 0) ||
|
||||||
(tabName === 'visibleMedia' && this.lastMediaFetchCount !== 0)
|
(tab === 'media' && this.lastMediaFetchCount !== 0)
|
||||||
) {
|
) {
|
||||||
return ` (${length}+)`
|
return ` (${length}+)`
|
||||||
}
|
}
|
||||||
|
@ -210,24 +230,11 @@ const Search = {
|
||||||
},
|
},
|
||||||
onResultTabSwitch (key) {
|
onResultTabSwitch (key) {
|
||||||
this.currentResultTab = key
|
this.currentResultTab = key
|
||||||
this.preferredTab = key
|
this.hasUserSelectedTab = true
|
||||||
this.loading = false
|
|
||||||
},
|
},
|
||||||
getActiveTab () {
|
getFirstTabWithResults () {
|
||||||
const available = {
|
for (const tab of allSearchTypes) {
|
||||||
statuses: this.visibleStatuses.length > 0,
|
if (this.getVisibleLength(tab) > 0) {
|
||||||
media: this.visibleMedia.length > 0,
|
|
||||||
accounts: this.users.length > 0,
|
|
||||||
hashtags: this.hashtags.length > 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
if (available[this.preferredTab]) {
|
|
||||||
return this.preferredTab
|
|
||||||
}
|
|
||||||
|
|
||||||
const tabOrder = ['statuses', 'media', 'accounts', 'hashtags']
|
|
||||||
for (const tab of tabOrder) {
|
|
||||||
if (available[tab]) {
|
|
||||||
return tab
|
return tab
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -236,6 +243,17 @@ const Search = {
|
||||||
},
|
},
|
||||||
lastHistoryRecord (hashtag) {
|
lastHistoryRecord (hashtag) {
|
||||||
return hashtag.history && hashtag.history[0]
|
return hashtag.history && hashtag.history[0]
|
||||||
|
},
|
||||||
|
getVisibleLength (tab) {
|
||||||
|
if (tab === 'statuses') {
|
||||||
|
return this.visibleStatuses.length
|
||||||
|
} else if (tab === 'media') {
|
||||||
|
return this.visibleMedia.length
|
||||||
|
} else if (tab === 'accounts') {
|
||||||
|
return this.users.length
|
||||||
|
} else if (tab === 'hashtags') {
|
||||||
|
return this.hashtags.length
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,23 +27,7 @@
|
||||||
<FAIcon icon="search" />
|
<FAIcon icon="search" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div v-if="loadedInitially">
|
||||||
v-if="
|
|
||||||
loading &&
|
|
||||||
visibleStatuses.length === 0 &&
|
|
||||||
visibleMedia.length === 0 &&
|
|
||||||
users.length === 0 &&
|
|
||||||
hashtags.length === 0
|
|
||||||
"
|
|
||||||
class="text-center loading-icon"
|
|
||||||
>
|
|
||||||
<FAIcon
|
|
||||||
icon="circle-notch"
|
|
||||||
spin
|
|
||||||
size="lg"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="loadedInitially">
|
|
||||||
<div class="search-nav-heading">
|
<div class="search-nav-heading">
|
||||||
<tab-switcher
|
<tab-switcher
|
||||||
ref="tabSwitcher"
|
ref="tabSwitcher"
|
||||||
|
@ -52,16 +36,16 @@
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
key="statuses"
|
key="statuses"
|
||||||
:label="$t('user_card.statuses') + resultCount('visibleStatuses')"
|
:label="$t('user_card.statuses') + resultCount('statuses')"
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
v-if="canSearchMediaPosts"
|
v-if="canSearchMediaPosts"
|
||||||
key="media"
|
key="media"
|
||||||
:label="$t('user_card.media') + resultCount('visibleMedia')"
|
:label="$t('user_card.media') + resultCount('media')"
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
key="accounts"
|
key="accounts"
|
||||||
:label="$t('search.people') + resultCount('users')"
|
:label="$t('search.people') + resultCount('accounts')"
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
key="hashtags"
|
key="hashtags"
|
||||||
|
@ -71,6 +55,16 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
|
<div
|
||||||
|
v-if="!loadedInitially"
|
||||||
|
class="text-center loading-icon"
|
||||||
|
>
|
||||||
|
<FAIcon
|
||||||
|
icon="circle-notch"
|
||||||
|
spin
|
||||||
|
size="lg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div v-if="currentResultTab === 'statuses'">
|
<div v-if="currentResultTab === 'statuses'">
|
||||||
<Conversation
|
<Conversation
|
||||||
v-for="status in visibleStatuses"
|
v-for="status in visibleStatuses"
|
||||||
|
@ -80,7 +74,7 @@
|
||||||
:status-id="status.id"
|
:status-id="status.id"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
v-if="!loading && loadedInitially && lastStatusFetchCount > 0"
|
v-if="!loading['statuses'] && loadedInitially && lastStatusFetchCount > 0"
|
||||||
class="more-statuses-button button-unstyled -link -fullwidth"
|
class="more-statuses-button button-unstyled -link -fullwidth"
|
||||||
@click.prevent="search(searchTerm, 'statuses')"
|
@click.prevent="search(searchTerm, 'statuses')"
|
||||||
>
|
>
|
||||||
|
@ -89,7 +83,7 @@
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<div
|
<div
|
||||||
v-else-if="loading && statusesOffset > 0"
|
v-else-if="loading['statuses'] && statusesOffset > 0"
|
||||||
class="text-center loading-icon"
|
class="text-center loading-icon"
|
||||||
>
|
>
|
||||||
<FAIcon
|
<FAIcon
|
||||||
|
@ -99,7 +93,10 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="(visibleStatuses.length === 0 || lastStatusFetchCount === 0) && !loading && loadedInitially"
|
v-if="
|
||||||
|
(visibleStatuses.length === 0 || lastStatusFetchCount === 0) &&
|
||||||
|
!loading['statuses'] && loadedInitially
|
||||||
|
"
|
||||||
class="search-result-heading"
|
class="search-result-heading"
|
||||||
>
|
>
|
||||||
<h4>
|
<h4>
|
||||||
|
@ -116,7 +113,7 @@
|
||||||
:status-id="media.id"
|
:status-id="media.id"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
v-if="!loading && loadedInitially && lastMediaFetchCount > 0"
|
v-if="!loading['media'] && loadedInitially && lastMediaFetchCount > 0"
|
||||||
class="more-statuses-button button-unstyled -link -fullwidth"
|
class="more-statuses-button button-unstyled -link -fullwidth"
|
||||||
@click.prevent="search(searchTerm, 'media')"
|
@click.prevent="search(searchTerm, 'media')"
|
||||||
>
|
>
|
||||||
|
@ -125,7 +122,7 @@
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<div
|
<div
|
||||||
v-else-if="loading && mediaOffset > 0"
|
v-else-if="loading['media'] && mediaOffset > 0"
|
||||||
class="text-center loading-icon"
|
class="text-center loading-icon"
|
||||||
>
|
>
|
||||||
<FAIcon
|
<FAIcon
|
||||||
|
@ -135,7 +132,10 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="(visibleMedia.length === 0 || lastMediaFetchCount === 0) && !loading && loadedInitially"
|
v-if="
|
||||||
|
(visibleMedia.length === 0 || lastMediaFetchCount === 0) &&
|
||||||
|
!loading['media'] && loadedInitially
|
||||||
|
"
|
||||||
class="search-result-heading"
|
class="search-result-heading"
|
||||||
>
|
>
|
||||||
<h4>
|
<h4>
|
||||||
|
@ -145,7 +145,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="currentResultTab === 'accounts'">
|
<div v-else-if="currentResultTab === 'accounts'">
|
||||||
<div
|
<div
|
||||||
v-if="users.length === 0 && !loading && loadedInitially"
|
v-if="users.length === 0 && !loading['accounts'] && loadedInitially"
|
||||||
class="search-result-heading"
|
class="search-result-heading"
|
||||||
>
|
>
|
||||||
<h4>{{ $t('search.no_results') }}</h4>
|
<h4>{{ $t('search.no_results') }}</h4>
|
||||||
|
@ -159,7 +159,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="currentResultTab === 'hashtags'">
|
<div v-else-if="currentResultTab === 'hashtags'">
|
||||||
<div
|
<div
|
||||||
v-if="hashtags.length === 0 && !loading && loadedInitially"
|
v-if="hashtags.length === 0 && !loading['hashtags'] && loadedInitially"
|
||||||
class="search-result-heading"
|
class="search-result-heading"
|
||||||
>
|
>
|
||||||
<h4>{{ $t('search.no_results') }}</h4>
|
<h4>{{ $t('search.no_results') }}</h4>
|
||||||
|
@ -294,5 +294,4 @@
|
||||||
height: 3.5em;
|
height: 3.5em;
|
||||||
line-height: 3.5em;
|
line-height: 3.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
Loading…
Reference in a new issue