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:
itepechi 2023-11-09 07:12:43 +09:00
parent bdc1e9d9c6
commit e8b653737c
2 changed files with 126 additions and 109 deletions

View File

@ -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,23 +126,28 @@ 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) {
try {
if (searchType === 'media' && skipMediaSearch) { if (searchType === 'media' && skipMediaSearch) {
continue continue
} }
@ -162,20 +177,22 @@ const Search = {
this.hashtags = this.hashtags.concat(data.hashtags) this.hashtags = this.hashtags.concat(data.hashtags)
if (isNewSearch) { if (isNewSearch) {
this.currentResultTab = this.getActiveTab() if (!this.hasUserSelectedTab) {
this.currentResultTab = this.getFirstTabWithResults()
}
if (searchType === 'statuses' && data.statuses.length === 0) { if (searchType === 'statuses' && data.statuses.length === 0) {
// safe to assume that there are no media posts // Safe to assume that there are no media posts
skipMediaSearch = true skipMediaSearch = true
} }
} }
} catch (error) {
if ( console.error(error)
!loadedInitially && } finally {
Object.values(data).some((result) => result.length > 0) if (!this.loadedInitially && this.hasAtLeastOneResult) {
) {
// Show results on the first meaningful response // Show results on the first meaningful response
this.loadedInitially = true this.loadedInitially = true
} }
this.loading[searchType] = false
if (searchType === 'statuses') { if (searchType === 'statuses') {
// Offset from whatever we already have // Offset from whatever we already have
@ -187,21 +204,24 @@ const Search = {
this.lastMediaFetchCount = this.media.length - oldMediaLength 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
}
} }
} }
} }

View File

@ -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>