feat: search for following when viewing accounts in a list (#3272)
This commit is contained in:
parent
c3b3f0fc4f
commit
b0f301843b
7 changed files with 383 additions and 175 deletions
|
@ -44,6 +44,7 @@ async function edit() {
|
||||||
<button
|
<button
|
||||||
text-sm p2 border-1 transition-colors
|
text-sm p2 border-1 transition-colors
|
||||||
border-dark
|
border-dark
|
||||||
|
bg-base
|
||||||
btn-action-icon
|
btn-action-icon
|
||||||
@click="edit"
|
@click="edit"
|
||||||
>
|
>
|
||||||
|
|
23
components/list/AccountSearchResult.vue
Normal file
23
components/list/AccountSearchResult.vue
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { SearchResult } from '~/composables/masto/search'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
result: SearchResult
|
||||||
|
active: boolean
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CommonScrollIntoView
|
||||||
|
as="div"
|
||||||
|
:active="active"
|
||||||
|
py2 block px2
|
||||||
|
:aria-selected="active"
|
||||||
|
:class="{ 'bg-active': active }"
|
||||||
|
>
|
||||||
|
<AccountInfo
|
||||||
|
v-if="result.type === 'account'"
|
||||||
|
:account="result.data"
|
||||||
|
/>
|
||||||
|
</CommonScrollIntoView>
|
||||||
|
</template>
|
|
@ -91,7 +91,7 @@ function activate() {
|
||||||
</div>
|
</div>
|
||||||
<!-- Results -->
|
<!-- Results -->
|
||||||
<div left-0 top-11 absolute w-full z-10 group-focus-within="pointer-events-auto visible" invisible pointer-events-none>
|
<div left-0 top-11 absolute w-full z-10 group-focus-within="pointer-events-auto visible" invisible pointer-events-none>
|
||||||
<div w-full bg-base border="~ base" rounded-3 max-h-100 overflow-auto py2>
|
<div w-full bg-base border="~ base" rounded-3 max-h-100 overflow-auto :class="results.length === 0 ? 'py2' : null">
|
||||||
<span v-if="query.trim().length === 0" block text-center text-sm text-secondary>
|
<span v-if="query.trim().length === 0" block text-center text-sm text-secondary>
|
||||||
{{ t('search.search_desc') }}
|
{{ t('search.search_desc') }}
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -226,7 +226,9 @@
|
||||||
"manage": "Manage lists",
|
"manage": "Manage lists",
|
||||||
"modify_account": "Modify lists with account",
|
"modify_account": "Modify lists with account",
|
||||||
"remove_account": "Remove account from list",
|
"remove_account": "Remove account from list",
|
||||||
"save": "Save changes"
|
"save": "Save changes",
|
||||||
|
"search_following_desc": "Search for people you are following",
|
||||||
|
"search_following_placeholder": "Search among people you follow"
|
||||||
},
|
},
|
||||||
"magic_keys": {
|
"magic_keys": {
|
||||||
"dialog_header": "Keyboard shortcuts",
|
"dialog_header": "Keyboard shortcuts",
|
||||||
|
|
|
@ -85,7 +85,7 @@
|
||||||
"iso-639-1": "^3.0.0",
|
"iso-639-1": "^3.0.0",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"lru-cache": "^11.0.0",
|
"lru-cache": "^11.0.0",
|
||||||
"masto": "^6.10.1",
|
"masto": "^6.10.4",
|
||||||
"mocked-exports": "^0.1.1",
|
"mocked-exports": "^0.1.1",
|
||||||
"node-emoji": "^2.1.3",
|
"node-emoji": "^2.1.3",
|
||||||
"node-mock-http": "^1.0.0",
|
"node-mock-http": "^1.0.0",
|
||||||
|
|
|
@ -1,16 +1,160 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { mastodon } from 'masto'
|
||||||
|
import AccountSearchResult from '~/components/list/AccountSearchResult.vue'
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
name: 'list-accounts',
|
name: 'list-accounts',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const inputRef = ref<HTMLInputElement>()
|
||||||
|
defineExpose({
|
||||||
|
inputRef,
|
||||||
|
})
|
||||||
|
|
||||||
const params = useRoute().params
|
const params = useRoute().params
|
||||||
const listId = computed(() => params.list as string)
|
const listId = computed(() => params.list as string)
|
||||||
|
|
||||||
const paginator = useMastoClient().v1.lists.$select(listId.value).accounts.list()
|
const mastoListAccounts = useMastoClient().v1.lists.$select(listId.value).accounts
|
||||||
|
const paginator = mastoListAccounts.list()
|
||||||
|
|
||||||
|
// the limit parameter is set to 1000 while masto.js issue is still open: https://github.com/neet/masto.js/issues/1282
|
||||||
|
const accountsInList = ref((await useMastoClient().v1.lists.$select(listId.value).accounts.list({ limit: 1000 })))
|
||||||
|
|
||||||
|
const paginatorRef = ref()
|
||||||
|
|
||||||
|
// search stuff
|
||||||
|
const query = ref('')
|
||||||
|
const el = ref<HTMLElement>()
|
||||||
|
const { accounts, loading } = useSearch(query, {
|
||||||
|
following: true,
|
||||||
|
})
|
||||||
|
const { focused } = useFocusWithin(el)
|
||||||
|
const index = ref(0)
|
||||||
|
|
||||||
|
function isInCurrentList(userId: string) {
|
||||||
|
return accountsInList.value.map(account => account.id).includes(userId)
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = computed(() => {
|
||||||
|
if (query.value.length === 0)
|
||||||
|
return []
|
||||||
|
return [...accounts.value]
|
||||||
|
})
|
||||||
|
|
||||||
|
// Reset index when results change
|
||||||
|
watch([results, focused], () => index.value = -1)
|
||||||
|
|
||||||
|
function addAccount(account: mastodon.v1.Account) {
|
||||||
|
try {
|
||||||
|
mastoListAccounts.create({ accountIds: [account.id] })
|
||||||
|
accountsInList.value.push(account)
|
||||||
|
paginatorRef.value?.createEntry(account)
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeAccount(account: mastodon.v1.Account) {
|
||||||
|
try {
|
||||||
|
mastoListAccounts.remove({ accountIds: [account.id] })
|
||||||
|
const accountIdsInList = accountsInList.value.map(account => account.id)
|
||||||
|
const index = accountIdsInList.indexOf(account.id)
|
||||||
|
if (index > -1) {
|
||||||
|
accountsInList.value.splice(index, 1)
|
||||||
|
paginatorRef.value?.removeEntry(account.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<CommonPaginator :paginator="paginator">
|
<!-- Search Accounts You Follow -->
|
||||||
|
<div ref="el" relative group>
|
||||||
|
<form
|
||||||
|
border="t base"
|
||||||
|
p-4 w-full
|
||||||
|
flex="~ wrap" relative gap-3
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
bg-base border="~ base" flex-1 h10 ps-1 pe-4 rounded-2 w-full flex="~ row"
|
||||||
|
items-center relative focus-within:box-shadow-outline gap-3
|
||||||
|
ps-4
|
||||||
|
>
|
||||||
|
<div i-ri:search-2-line pointer-events-none text-secondary mt="1px" class="rtl-flip" />
|
||||||
|
<input
|
||||||
|
ref="inputRef"
|
||||||
|
v-model="query"
|
||||||
|
bg-transparent
|
||||||
|
outline="focus:none"
|
||||||
|
ps-3
|
||||||
|
rounded-3
|
||||||
|
pb="1px"
|
||||||
|
h-full
|
||||||
|
w-full
|
||||||
|
placeholder-text-secondary
|
||||||
|
:placeholder="$t('list.search_following_placeholder')"
|
||||||
|
@keydown.esc.prevent="inputRef?.blur()"
|
||||||
|
@keydown.enter.prevent
|
||||||
|
>
|
||||||
|
<button v-if="query.length" btn-action-icon text-secondary @click="query = ''; inputRef?.focus()">
|
||||||
|
<span aria-hidden="true" class="i-ri:close-line" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Results -->
|
||||||
|
<div left-0 top-18 absolute w-full z-10 group-focus-within="pointer-events-auto visible" invisible pointer-events-none>
|
||||||
|
<div w-full bg-base border="~ dark" rounded-3 max-h-100 overflow-auto :class="results.length === 0 ? 'py2' : null">
|
||||||
|
<span v-if="query.trim().length === 0" block text-center text-sm text-secondary>
|
||||||
|
{{ $t('list.search_following_desc') }}
|
||||||
|
</span>
|
||||||
|
<template v-else-if="!loading">
|
||||||
|
<template v-if="results.length > 0">
|
||||||
|
<div
|
||||||
|
v-for="(result, i) in results"
|
||||||
|
:key="result.id"
|
||||||
|
flex
|
||||||
|
border="b base"
|
||||||
|
py2 px4
|
||||||
|
hover:bg-active justify-between transition-100 items-center
|
||||||
|
>
|
||||||
|
<AccountSearchResult
|
||||||
|
:active="index === parseInt(i.toString())"
|
||||||
|
:result="result"
|
||||||
|
:tabindex="focused ? 0 : -1"
|
||||||
|
/>
|
||||||
|
<CommonTooltip :content="isInCurrentList(result.id) ? $t('list.remove_account') : $t('list.add_account')">
|
||||||
|
<button
|
||||||
|
text-sm p2 border-1 transition-colors
|
||||||
|
border-dark
|
||||||
|
btn-action-icon
|
||||||
|
bg-base
|
||||||
|
:hover="isInCurrentList(result.id) ? 'text-red' : 'text-green'"
|
||||||
|
@click=" () => isInCurrentList(result.id) ? removeAccount(result.data) : addAccount(result.data) "
|
||||||
|
>
|
||||||
|
<span :class="isInCurrentList(result.id) ? 'i-ri:user-unfollow-line' : 'i-ri:user-add-line'" />
|
||||||
|
</button>
|
||||||
|
</CommonTooltip>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<span v-else block text-center text-sm text-secondary>
|
||||||
|
{{ $t('search.search_empty') }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<div v-else>
|
||||||
|
<SearchResultSkeleton />
|
||||||
|
<SearchResultSkeleton />
|
||||||
|
<SearchResultSkeleton />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CommonPaginator ref="paginatorRef" :paginator="paginator">
|
||||||
<template #default="{ item }">
|
<template #default="{ item }">
|
||||||
<ListAccount
|
<ListAccount
|
||||||
:account="item"
|
:account="item"
|
||||||
|
|
378
pnpm-lock.yaml
generated
378
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue