integrate withLoadMore HOC into List
This commit is contained in:
parent
4da16acc27
commit
503309890f
16 changed files with 449 additions and 434 deletions
58
src/components/list/list.css
Normal file
58
src/components/list/list.css
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
.List {
|
||||
--__line-height: 1.5em;
|
||||
--__horizontal-gap: 0.75em;
|
||||
--__vertical-gap: 0.5em;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.list {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.list-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px dotted var(--border);
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: var(--__vertical-gap) var(--__horizontal-gap);
|
||||
border-bottom: 1px solid;
|
||||
border-bottom-color: var(--border);
|
||||
|
||||
.actions {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
padding: 0.9em;
|
||||
text-align: center;
|
||||
|
||||
a {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox-wrapper {
|
||||
padding-right: var(--__horizontal-gap);
|
||||
flex: none;
|
||||
}
|
||||
|
||||
&.-scrollable {
|
||||
overflow-y: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.list {
|
||||
overflow-y: auto;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
152
src/components/list/list.js
Normal file
152
src/components/list/list.js
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
import { isEmpty } from 'lodash'
|
||||
|
||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
|
||||
const List = {
|
||||
props: {
|
||||
boxOnly: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
fetchFunction: {
|
||||
type: Function,
|
||||
default: null,
|
||||
},
|
||||
itemsFunction: {
|
||||
type: Function,
|
||||
default: null,
|
||||
},
|
||||
getKey: {
|
||||
type: Function,
|
||||
default: (item) => item.id,
|
||||
},
|
||||
getClass: {
|
||||
type: Function,
|
||||
default: () => '',
|
||||
},
|
||||
nonInteractive: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
scrollable: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
selectable: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
components: {
|
||||
Checkbox,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
bottomedOut: false,
|
||||
error: false,
|
||||
dynamicItems: this.itemsFunction ? [] : null,
|
||||
selected: [],
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
allKeys() {
|
||||
return this.actualItems.map(this.getKey)
|
||||
},
|
||||
filteredSelected() {
|
||||
return this.allKeys.filter((key) => this.selected.indexOf(key) !== -1)
|
||||
},
|
||||
allSelected() {
|
||||
return this.filteredSelected.length === this.actualItems.length
|
||||
},
|
||||
noneSelected() {
|
||||
return this.filteredSelected.length === 0
|
||||
},
|
||||
someSelected() {
|
||||
return !this.allSelected && !this.noneSelected
|
||||
},
|
||||
actualItems() {
|
||||
return this.dynamicItems || this.actualItems
|
||||
},
|
||||
},
|
||||
created() {
|
||||
if (this.fetchFunction) {
|
||||
window.addEventListener('scroll', this.scrollLoad)
|
||||
|
||||
if (this.dynamicItems.length === 0) {
|
||||
this.fetchEntries()
|
||||
}
|
||||
}
|
||||
},
|
||||
unmounted() {
|
||||
window.removeEventListener('scroll', this.scrollLoad)
|
||||
},
|
||||
methods: {
|
||||
// Entries is not a computed because computed can't track the dynamic
|
||||
// selector for changes and won't trigger after fetch.
|
||||
updateEntries(newEntries) {
|
||||
this.dynamicItems = this.itemsFunction(newEntries)
|
||||
},
|
||||
fetchEntries() {
|
||||
if (!this.loading) {
|
||||
this.loading = true
|
||||
this.error = false
|
||||
this.fetchFunction()
|
||||
.then((newEntries) => {
|
||||
this.loading = false
|
||||
this.bottomedOut = isEmpty(newEntries)
|
||||
return newEntries
|
||||
})
|
||||
.catch((error) => {
|
||||
this.loading = false
|
||||
this.error = error
|
||||
})
|
||||
.finally((newEntries) => {
|
||||
this.updateEntries(newEntries)
|
||||
})
|
||||
}
|
||||
},
|
||||
scrollLoad(e) {
|
||||
if (this.fetchFunction) {
|
||||
const bodyBRect = document.body.getBoundingClientRect()
|
||||
const height = Math.max(bodyBRect.height, -bodyBRect.y)
|
||||
if (
|
||||
this.loading === false &&
|
||||
this.bottomedOut === false &&
|
||||
this.$el.offsetHeight > 0 &&
|
||||
window.innerHeight + window.pageYOffset >= height - 750
|
||||
) {
|
||||
this.fetchEntries()
|
||||
}
|
||||
}
|
||||
},
|
||||
isSelected(item) {
|
||||
return this.filteredSelected.indexOf(this.getKey(item)) !== -1
|
||||
},
|
||||
toggle(checked, item) {
|
||||
console.log('TOGGLE', checked, item)
|
||||
const key = this.getKey(item)
|
||||
const oldChecked = this.isSelected(key)
|
||||
if (checked !== oldChecked) {
|
||||
if (checked) {
|
||||
this.selected.push(key)
|
||||
} else {
|
||||
this.selected.splice(this.selected.indexOf(key), 1)
|
||||
}
|
||||
}
|
||||
},
|
||||
toggleAll(value) {
|
||||
if (value) {
|
||||
this.selected = this.allKeys.slice(0)
|
||||
} else {
|
||||
this.selected = []
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default List
|
||||
|
|
@ -1,49 +1,91 @@
|
|||
<template>
|
||||
<div
|
||||
class="list"
|
||||
role="list"
|
||||
class="List"
|
||||
:class="{ '-scrollable': scrollable }"
|
||||
>
|
||||
<div
|
||||
v-for="item in items"
|
||||
:key="getKey(item)"
|
||||
class="list-item"
|
||||
:class="[getClass(item), nonInteractive ? '-non-interactive' : '']"
|
||||
role="listitem"
|
||||
v-if="selectable"
|
||||
class="header"
|
||||
>
|
||||
<slot
|
||||
name="item"
|
||||
:item="item"
|
||||
/>
|
||||
<div class="checkbox-wrapper">
|
||||
<Checkbox
|
||||
:model-value="allSelected"
|
||||
:indeterminate="someSelected"
|
||||
@update:model-value="toggleAll"
|
||||
>
|
||||
{{ $t('selectable_list.select_all') }}
|
||||
</Checkbox>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<slot
|
||||
name="header"
|
||||
:selected="filteredSelected"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="items.length === 0 && !!$slots.empty"
|
||||
class="list-empty-content faint"
|
||||
class="list"
|
||||
role="list"
|
||||
>
|
||||
<slot name="empty" />
|
||||
<slot name="load" />
|
||||
<div
|
||||
v-for="item in actualItems"
|
||||
:key="getKey(item)"
|
||||
class="list-item"
|
||||
:class="[getClass(item), nonInteractive ? '-non-interactive' : '']"
|
||||
role="listitem"
|
||||
>
|
||||
<div
|
||||
v-if="selectable"
|
||||
class="checkbox-wrapper"
|
||||
>
|
||||
<Checkbox
|
||||
:model-value="isSelected(item)"
|
||||
@update:model-value="checked => toggle(checked, item)"
|
||||
@click.stop
|
||||
/>
|
||||
</div>
|
||||
<slot
|
||||
name="item"
|
||||
:item="item"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="actualItems.length === 0 && !!$slots.empty"
|
||||
class="list-empty-content faint"
|
||||
>
|
||||
<slot name="empty" />
|
||||
<slot name="load" />
|
||||
</div>
|
||||
<div class="footer">
|
||||
<button
|
||||
v-if="error"
|
||||
class="button-unstyled -link -fullwidth alert error"
|
||||
@click="fetchEntries"
|
||||
>
|
||||
{{ $t('general.generic_error') }}
|
||||
{{ error }}
|
||||
</button>
|
||||
<FAIcon
|
||||
v-else-if="loading"
|
||||
spin
|
||||
icon="circle-notch"
|
||||
/>
|
||||
<a
|
||||
v-else-if="!bottomedOut"
|
||||
@click="fetchEntries"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
{{ $t('general.more') }}
|
||||
</a>
|
||||
<span v-else>
|
||||
{{ $t('general.no_more') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
getKey: {
|
||||
type: Function,
|
||||
default: (item) => item.id,
|
||||
},
|
||||
getClass: {
|
||||
type: Function,
|
||||
default: () => '',
|
||||
},
|
||||
nonInteractive: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<script src="./list.js"></script>
|
||||
|
||||
<style src="./list.css"></style>
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@
|
|||
flex-direction: column;
|
||||
|
||||
.list {
|
||||
flex: 1 1 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&-item-inner {
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@
|
|||
|
||||
.tab-slot-wrapper {
|
||||
flex: 1 1 auto;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
padding: 0 1em;
|
||||
overflow-y: auto;
|
||||
|
|
@ -56,10 +57,16 @@
|
|||
}
|
||||
|
||||
&.-full-height {
|
||||
height: 100%;
|
||||
> * {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&.-full-width.-full-height {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { get, map, reject } from 'lodash'
|
||||
|
||||
import withLoadMore from 'src/components/../hocs/with_load_more/with_load_more'
|
||||
import withSubscription from 'src/components/../hocs/with_subscription/with_subscription'
|
||||
import Autosuggest from 'src/components/autosuggest/autosuggest.vue'
|
||||
import BlockCard from 'src/components/block_card/block_card.vue'
|
||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
import DomainMuteCard from 'src/components/domain_mute_card/domain_mute_card.vue'
|
||||
import List from 'src/components/list/list.vue'
|
||||
import MuteCard from 'src/components/mute_card/mute_card.vue'
|
||||
import ProgressButton from 'src/components/progress_button/progress_button.vue'
|
||||
import SelectableList from 'src/components/selectable_list/selectable_list.vue'
|
||||
|
|
@ -14,32 +14,6 @@ import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
|
|||
import { useInstanceStore } from 'src/stores/instance.js'
|
||||
import { useOAuthTokensStore } from 'src/stores/oauth_tokens.js'
|
||||
|
||||
const BlockList = withLoadMore({
|
||||
fetch: (props, $store) => $store.dispatch('fetchBlocks'),
|
||||
select: (props, $store) =>
|
||||
get($store.state.users.currentUser, 'blockIds', []),
|
||||
destroy: () => {
|
||||
/* no-op */
|
||||
},
|
||||
childPropName: 'items',
|
||||
})(SelectableList)
|
||||
|
||||
const MuteList = withLoadMore({
|
||||
fetch: (props, $store) => $store.dispatch('fetchMutes'),
|
||||
select: (props, $store) => get($store.state.users.currentUser, 'muteIds', []),
|
||||
destroy: () => {
|
||||
/* no-op */
|
||||
},
|
||||
childPropName: 'items',
|
||||
})(SelectableList)
|
||||
|
||||
const DomainMuteList = withSubscription({
|
||||
fetch: (props, $store) => $store.dispatch('fetchDomainMutes'),
|
||||
select: (props, $store) =>
|
||||
get($store.state.users.currentUser, 'domainMutes', []),
|
||||
childPropName: 'items',
|
||||
})(SelectableList)
|
||||
|
||||
const MutesAndBlocks = {
|
||||
data() {
|
||||
return {
|
||||
|
|
@ -52,12 +26,10 @@ const MutesAndBlocks = {
|
|||
},
|
||||
components: {
|
||||
TabSwitcher,
|
||||
BlockList,
|
||||
MuteList,
|
||||
DomainMuteList,
|
||||
BlockCard,
|
||||
MuteCard,
|
||||
DomainMuteCard,
|
||||
BlockCard,
|
||||
List,
|
||||
MuteCard,
|
||||
ProgressButton,
|
||||
Autosuggest,
|
||||
Checkbox,
|
||||
|
|
@ -102,6 +74,15 @@ const MutesAndBlocks = {
|
|||
})
|
||||
.join('\n')
|
||||
},
|
||||
getBlocks() {
|
||||
return get(this.$store.state.users.currentUser, 'blockIds', [])
|
||||
},
|
||||
getMutes() {
|
||||
return get(this.$store.state.users.currentUser, 'muteIds', [])
|
||||
},
|
||||
getDomainMutes() {
|
||||
return get(this.$store.state.users.currentUser, 'domainMutes', [])
|
||||
},
|
||||
activateTab(tabName) {
|
||||
this.activeTab = tabName
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
.mutes-and-blocks-tab {
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
|
||||
.usersearch-wrapper {
|
||||
padding: 1em;
|
||||
|
|
@ -26,4 +26,12 @@
|
|||
margin-top: 1em;
|
||||
width: 10em;
|
||||
}
|
||||
|
||||
.blocks, .mutes {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100%;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
<template>
|
||||
<tab-switcher
|
||||
:scrollable-tabs="true"
|
||||
class="mutes-and-blocks-tab"
|
||||
>
|
||||
<div :label="$t('settings.blocks_tab')">
|
||||
:scrollable-tabs="true"
|
||||
>
|
||||
<div
|
||||
class="blocks"
|
||||
:label="$t('settings.user_blocks')"
|
||||
>
|
||||
<div class="usersearch-wrapper">
|
||||
<Autosuggest
|
||||
:filter="filterUnblockedUsers"
|
||||
|
|
@ -17,9 +20,12 @@
|
|||
</template>
|
||||
</Autosuggest>
|
||||
</div>
|
||||
<BlockList
|
||||
:refresh="true"
|
||||
<List
|
||||
:get-key="i => i"
|
||||
:items-function="getBlocks"
|
||||
:fetch-function="() => $store.dispatch('fetchBlocks')"
|
||||
scrollable
|
||||
selectable
|
||||
>
|
||||
<template #header="{selected}">
|
||||
<div class="bulk-actions">
|
||||
|
|
@ -51,103 +57,106 @@
|
|||
<template #empty>
|
||||
{{ $t('settings.no_blocks') }}
|
||||
</template>
|
||||
</BlockList>
|
||||
</List>
|
||||
</div>
|
||||
|
||||
<div :label="$t('settings.mutes_tab')">
|
||||
<tab-switcher>
|
||||
<div :label="$t('settings.user_mutes')">
|
||||
<div class="usersearch-wrapper">
|
||||
<Autosuggest
|
||||
:filter="filterUnMutedUsers"
|
||||
:query="queryUserIds"
|
||||
:placeholder="$t('settings.search_user_to_mute')"
|
||||
<div class="mutes" :label="$t('settings.user_mutes2')">
|
||||
<div class="usersearch-wrapper">
|
||||
<Autosuggest
|
||||
:filter="filterUnMutedUsers"
|
||||
:query="queryUserIds"
|
||||
:placeholder="$t('settings.search_user_to_mute')"
|
||||
>
|
||||
<template #default="row">
|
||||
<MuteCard
|
||||
:user-id="row.item"
|
||||
/>
|
||||
</template>
|
||||
</Autosuggest>
|
||||
</div>
|
||||
<List
|
||||
:get-key="i => i"
|
||||
:items-function="getMutes"
|
||||
:fetch-function="() => $store.dispatch('fetchMutes')"
|
||||
scrollable
|
||||
selectable
|
||||
>
|
||||
<template #header="{selected}">
|
||||
<div class="bulk-actions">
|
||||
<ProgressButton
|
||||
v-if="selected.length > 0"
|
||||
class="btn button-default"
|
||||
:click="() => muteUsers(selected)"
|
||||
>
|
||||
<template #default="row">
|
||||
<MuteCard
|
||||
:user-id="row.item"
|
||||
/>
|
||||
{{ $t('user_card.mute') }}
|
||||
<template #progress>
|
||||
{{ $t('user_card.mute_progress') }}
|
||||
</template>
|
||||
</Autosuggest>
|
||||
</ProgressButton>
|
||||
<ProgressButton
|
||||
v-if="selected.length > 0"
|
||||
class="btn button-default"
|
||||
:click="() => unmuteUsers(selected)"
|
||||
>
|
||||
{{ $t('user_card.unmute') }}
|
||||
<template #progress>
|
||||
{{ $t('user_card.unmute_progress') }}
|
||||
</template>
|
||||
</ProgressButton>
|
||||
</div>
|
||||
<MuteList
|
||||
:refresh="true"
|
||||
:get-key="i => i"
|
||||
>
|
||||
<template #header="{selected}">
|
||||
<div class="bulk-actions">
|
||||
<ProgressButton
|
||||
v-if="selected.length > 0"
|
||||
class="btn button-default"
|
||||
:click="() => muteUsers(selected)"
|
||||
>
|
||||
{{ $t('user_card.mute') }}
|
||||
<template #progress>
|
||||
{{ $t('user_card.mute_progress') }}
|
||||
</template>
|
||||
</ProgressButton>
|
||||
<ProgressButton
|
||||
v-if="selected.length > 0"
|
||||
class="btn button-default"
|
||||
:click="() => unmuteUsers(selected)"
|
||||
>
|
||||
{{ $t('user_card.unmute') }}
|
||||
<template #progress>
|
||||
{{ $t('user_card.unmute_progress') }}
|
||||
</template>
|
||||
</ProgressButton>
|
||||
</div>
|
||||
</template>
|
||||
<template #item="{item}">
|
||||
<MuteCard :user-id="item" />
|
||||
</template>
|
||||
<template #empty>
|
||||
{{ $t('settings.no_mutes') }}
|
||||
</template>
|
||||
</MuteList>
|
||||
</div>
|
||||
</template>
|
||||
<template #item="{item}">
|
||||
<MuteCard :user-id="item" />
|
||||
</template>
|
||||
<template #empty>
|
||||
{{ $t('settings.no_mutes') }}
|
||||
</template>
|
||||
</List>
|
||||
</div>
|
||||
|
||||
<div :label="$t('settings.domain_mutes')">
|
||||
<div class="domain-mute-form">
|
||||
<Autosuggest
|
||||
:filter="filterUnMutedDomains"
|
||||
:query="queryKnownDomains"
|
||||
:placeholder="$t('settings.type_domains_to_mute')"
|
||||
<div :label="$t('settings.domain_mutes2')">
|
||||
<div class="domain-mute-form">
|
||||
<Autosuggest
|
||||
:filter="filterUnMutedDomains"
|
||||
:query="queryKnownDomains"
|
||||
:placeholder="$t('settings.type_domains_to_mute')"
|
||||
>
|
||||
<template #default="row">
|
||||
<DomainMuteCard
|
||||
:domain="row.item"
|
||||
/>
|
||||
</template>
|
||||
</Autosuggest>
|
||||
</div>
|
||||
<List
|
||||
:get-key="i => i"
|
||||
:items-function="getDomainMutes"
|
||||
:fetch-function="() => $store.dispatch('fetchDomainMutes')"
|
||||
scrollable
|
||||
selectable
|
||||
>
|
||||
<template #header="{selected}">
|
||||
<div class="bulk-actions">
|
||||
<ProgressButton
|
||||
v-if="selected.length > 0"
|
||||
class="btn button-default"
|
||||
:click="() => unmuteDomains(selected)"
|
||||
>
|
||||
<template #default="row">
|
||||
<DomainMuteCard
|
||||
:domain="row.item"
|
||||
/>
|
||||
{{ $t('domain_mute_card.unmute') }}
|
||||
<template #progress>
|
||||
{{ $t('domain_mute_card.unmute_progress') }}
|
||||
</template>
|
||||
</Autosuggest>
|
||||
</ProgressButton>
|
||||
</div>
|
||||
<DomainMuteList
|
||||
:refresh="true"
|
||||
:get-key="i => i"
|
||||
>
|
||||
<template #header="{selected}">
|
||||
<div class="bulk-actions">
|
||||
<ProgressButton
|
||||
v-if="selected.length > 0"
|
||||
class="btn button-default"
|
||||
:click="() => unmuteDomains(selected)"
|
||||
>
|
||||
{{ $t('domain_mute_card.unmute') }}
|
||||
<template #progress>
|
||||
{{ $t('domain_mute_card.unmute_progress') }}
|
||||
</template>
|
||||
</ProgressButton>
|
||||
</div>
|
||||
</template>
|
||||
<template #item="{item}">
|
||||
<DomainMuteCard :domain="item" />
|
||||
</template>
|
||||
<template #empty>
|
||||
{{ $t('settings.no_mutes') }}
|
||||
</template>
|
||||
</DomainMuteList>
|
||||
</div>
|
||||
</tab-switcher>
|
||||
</template>
|
||||
<template #item="{item}">
|
||||
{{ item }}
|
||||
<DomainMuteCard :domain="item" />
|
||||
</template>
|
||||
<template #empty>
|
||||
{{ $t('settings.no_mutes') }}
|
||||
</template>
|
||||
</List>
|
||||
</div>
|
||||
</tab-switcher>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@
|
|||
|
||||
.contents.scrollable-tabs {
|
||||
flex-basis: 0;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import List from 'src/components/list/list.vue'
|
|||
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
|
||||
import Timeline from 'src/components/timeline/timeline.vue'
|
||||
import UserCard from 'src/components/user_card/user_card.vue'
|
||||
import withLoadMore from '../../hocs/with_load_more/with_load_more'
|
||||
|
||||
import { useInstanceStore } from 'src/stores/instance.js'
|
||||
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
|
||||
|
|
@ -19,28 +18,6 @@ import { faCircleNotch } from '@fortawesome/free-solid-svg-icons'
|
|||
|
||||
library.add(faCircleNotch)
|
||||
|
||||
const FollowerList = withLoadMore({
|
||||
fetch: (props, $store) => $store.dispatch('fetchFollowers', props.userId),
|
||||
select: (props, $store) =>
|
||||
get($store.getters.findUser(props.userId), 'followerIds', []).map((id) =>
|
||||
$store.getters.findUser(id),
|
||||
),
|
||||
destroy: (props, $store) => $store.dispatch('clearFollowers', props.userId),
|
||||
childPropName: 'items',
|
||||
additionalPropNames: ['userId'],
|
||||
})(List)
|
||||
|
||||
const FriendList = withLoadMore({
|
||||
fetch: (props, $store) => $store.dispatch('fetchFriends', props.userId),
|
||||
select: (props, $store) =>
|
||||
get($store.getters.findUser(props.userId), 'friendIds', []).map((id) =>
|
||||
$store.getters.findUser(id),
|
||||
),
|
||||
destroy: (props, $store) => $store.dispatch('clearFriends', props.userId),
|
||||
childPropName: 'items',
|
||||
additionalPropNames: ['userId'],
|
||||
})(List)
|
||||
|
||||
const defaultTabKey = 'statuses'
|
||||
|
||||
const UserProfile = {
|
||||
|
|
@ -64,6 +41,8 @@ const UserProfile = {
|
|||
unmounted() {
|
||||
this.stopFetching()
|
||||
useInterfaceStore().setForeignProfileBackground(null)
|
||||
this.$store.dispatch('clearFollowers', this.userId)
|
||||
this.$store.dispatch('clearFriends', this.userId)
|
||||
},
|
||||
computed: {
|
||||
timeline() {
|
||||
|
|
@ -109,6 +88,18 @@ const UserProfile = {
|
|||
setFooterRef(el) {
|
||||
this.footerRef = el
|
||||
},
|
||||
getFriends() {
|
||||
return get(
|
||||
this.$store.getters.findUser(this.userId),
|
||||
'friendIds', []
|
||||
).map((id) => this.$store.getters.findUser(id))
|
||||
},
|
||||
getFollowers() {
|
||||
return get(
|
||||
this.$store.getters.findUser(this.userId),
|
||||
'followerIds', []
|
||||
).map((id) => this.$store.getters.findUser(id))
|
||||
},
|
||||
load(userNameOrId) {
|
||||
const startFetchingTimeline = (timeline, userId) => {
|
||||
// Clear timeline only if load another user's profile
|
||||
|
|
@ -203,8 +194,7 @@ const UserProfile = {
|
|||
components: {
|
||||
UserCard,
|
||||
Timeline,
|
||||
FollowerList,
|
||||
FriendList,
|
||||
List,
|
||||
FollowCard,
|
||||
TabSwitcher,
|
||||
Conversation,
|
||||
|
|
|
|||
|
|
@ -39,14 +39,15 @@
|
|||
:label="$t('user_card.followees')"
|
||||
:disabled="!user.friends_count"
|
||||
>
|
||||
<FriendList
|
||||
<List
|
||||
:user-id="userId"
|
||||
:non-interactive="true"
|
||||
:items-function="getFriends"
|
||||
:fetch-function="() => $store.dispatch('fetchFriends', userId)"
|
||||
>
|
||||
<template #item="{item}">
|
||||
<FollowCard :user="item" />
|
||||
</template>
|
||||
</FriendList>
|
||||
</List>
|
||||
</div>
|
||||
<div
|
||||
v-if="followersTabVisible"
|
||||
|
|
@ -55,9 +56,10 @@
|
|||
:label="$t('user_card.followers')"
|
||||
:disabled="!user.followers_count"
|
||||
>
|
||||
<FollowerList
|
||||
<List
|
||||
:user-id="userId"
|
||||
:non-interactive="true"
|
||||
:items-function="getFollowers"
|
||||
:fetch-function="() => $store.dispatch('fetchFollowers', userId)"
|
||||
>
|
||||
<template #item="{item}">
|
||||
<FollowCard
|
||||
|
|
@ -65,7 +67,7 @@
|
|||
:no-follows-you="isUs"
|
||||
/>
|
||||
</template>
|
||||
</FollowerList>
|
||||
</List>
|
||||
</div>
|
||||
<Timeline
|
||||
key="media"
|
||||
|
|
|
|||
|
|
@ -1,119 +0,0 @@
|
|||
// eslint-disable-next-line no-unused
|
||||
|
||||
import { isEmpty } from 'lodash'
|
||||
import { h } from 'vue'
|
||||
|
||||
import { getComponentProps } from '../../services/component_utils/component_utils'
|
||||
import './with_load_more.scss'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faCircleNotch } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon as FAIcon } from '@fortawesome/vue-fontawesome'
|
||||
|
||||
library.add(faCircleNotch)
|
||||
|
||||
const withLoadMore =
|
||||
({
|
||||
fetch, // function to fetch entries and return a promise
|
||||
select, // function to select data from store
|
||||
unmounted, // function called at "destroyed" lifecycle
|
||||
childPropName = 'entries', // name of the prop to be passed into the wrapped component
|
||||
additionalPropNames = [], // additional prop name list of the wrapper component
|
||||
}) =>
|
||||
(WrappedComponent) => {
|
||||
const originalProps = Object.keys(getComponentProps(WrappedComponent))
|
||||
const props = originalProps
|
||||
.filter((v) => v !== childPropName)
|
||||
.concat(additionalPropNames)
|
||||
|
||||
return {
|
||||
props,
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
bottomedOut: false,
|
||||
error: false,
|
||||
entries: [],
|
||||
}
|
||||
},
|
||||
created() {
|
||||
window.addEventListener('scroll', this.scrollLoad)
|
||||
if (this.entries.length === 0) {
|
||||
this.fetchEntries()
|
||||
}
|
||||
},
|
||||
unmounted() {
|
||||
window.removeEventListener('scroll', this.scrollLoad)
|
||||
unmounted && unmounted(this.$props, this.$store)
|
||||
},
|
||||
methods: {
|
||||
// Entries is not a computed because computed can't track the dynamic
|
||||
// selector for changes and won't trigger after fetch.
|
||||
updateEntries() {
|
||||
this.entries = select(this.$props, this.$store) || []
|
||||
},
|
||||
fetchEntries() {
|
||||
if (!this.loading) {
|
||||
this.loading = true
|
||||
this.error = false
|
||||
fetch(this.$props, this.$store)
|
||||
.then((newEntries) => {
|
||||
this.loading = false
|
||||
this.bottomedOut = isEmpty(newEntries)
|
||||
})
|
||||
.catch(() => {
|
||||
this.loading = false
|
||||
this.error = true
|
||||
})
|
||||
.finally(() => {
|
||||
this.updateEntries()
|
||||
})
|
||||
}
|
||||
},
|
||||
scrollLoad(e) {
|
||||
const bodyBRect = document.body.getBoundingClientRect()
|
||||
const height = Math.max(bodyBRect.height, -bodyBRect.y)
|
||||
if (
|
||||
this.loading === false &&
|
||||
this.bottomedOut === false &&
|
||||
this.$el.offsetHeight > 0 &&
|
||||
window.innerHeight + window.pageYOffset >= height - 750
|
||||
) {
|
||||
this.fetchEntries()
|
||||
}
|
||||
},
|
||||
},
|
||||
render() {
|
||||
const props = {
|
||||
...this.$props,
|
||||
[childPropName]: this.entries,
|
||||
}
|
||||
const children = this.$slots
|
||||
return (
|
||||
<div class="with-load-more">
|
||||
<WrappedComponent {...props}>{children}</WrappedComponent>
|
||||
<div class="with-load-more-footer">
|
||||
{this.error && (
|
||||
<button
|
||||
onClick={this.fetchEntries}
|
||||
class="button-unstyled -link -fullwidth alert error"
|
||||
>
|
||||
{this.$t('general.generic_error')}
|
||||
</button>
|
||||
)}
|
||||
{!this.error && this.loading && (
|
||||
<FAIcon spin icon="circle-notch" />
|
||||
)}
|
||||
{!this.error && !this.loading && !this.bottomedOut && (
|
||||
<a onClick={this.fetchEntries} role="button" tabindex="0">
|
||||
{this.$t('general.more')}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default withLoadMore
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
.with-load-more {
|
||||
&-footer {
|
||||
padding: 0.9em;
|
||||
text-align: center;
|
||||
border-top: 1px solid;
|
||||
border-top-color: var(--border);
|
||||
|
||||
.error {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
a {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,94 +0,0 @@
|
|||
// eslint-disable-next-line no-unused
|
||||
|
||||
import { isEmpty } from 'lodash'
|
||||
import { h } from 'vue'
|
||||
|
||||
import { getComponentProps } from '../../services/component_utils/component_utils'
|
||||
import './with_subscription.scss'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faCircleNotch } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon as FAIcon } from '@fortawesome/vue-fontawesome'
|
||||
|
||||
library.add(faCircleNotch)
|
||||
|
||||
const withSubscription =
|
||||
({
|
||||
fetch, // function to fetch entries and return a promise
|
||||
select, // function to select data from store
|
||||
childPropName = 'content', // name of the prop to be passed into the wrapped component
|
||||
additionalPropNames = [], // additional prop name list of the wrapper component
|
||||
}) =>
|
||||
(WrappedComponent) => {
|
||||
const originalProps = Object.keys(getComponentProps(WrappedComponent))
|
||||
const props = originalProps
|
||||
.filter((v) => v !== childPropName)
|
||||
.concat(additionalPropNames)
|
||||
|
||||
return {
|
||||
props: [
|
||||
...props,
|
||||
'refresh', // boolean saying to force-fetch data whenever created
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
error: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
fetchedData() {
|
||||
return select(this.$props, this.$store)
|
||||
},
|
||||
},
|
||||
created() {
|
||||
if (this.refresh || isEmpty(this.fetchedData)) {
|
||||
this.fetchData()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
fetchData() {
|
||||
if (!this.loading) {
|
||||
this.loading = true
|
||||
this.error = false
|
||||
fetch(this.$props, this.$store)
|
||||
.then(() => {
|
||||
this.loading = false
|
||||
})
|
||||
.catch(() => {
|
||||
this.error = true
|
||||
this.loading = false
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
render() {
|
||||
if (!this.error && !this.loading) {
|
||||
const props = {
|
||||
...this.$props,
|
||||
[childPropName]: this.fetchedData,
|
||||
}
|
||||
const children = this.$slots
|
||||
return (
|
||||
<div class="with-subscription">
|
||||
<WrappedComponent {...props}>{children}</WrappedComponent>
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<div class="with-subscription-loading">
|
||||
{this.error ? (
|
||||
<a onClick={this.fetchData} class="alert error">
|
||||
{this.$t('general.generic_error')}
|
||||
</a>
|
||||
) : (
|
||||
<FAIcon spin icon="circle-notch" />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default withSubscription
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
.with-subscription {
|
||||
&-loading {
|
||||
padding: 0.7em;
|
||||
text-align: center;
|
||||
|
||||
.error {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -86,6 +86,7 @@
|
|||
"apply": "Apply",
|
||||
"submit": "Submit",
|
||||
"more": "More",
|
||||
"no_more": "No more items",
|
||||
"loading": "Loading…",
|
||||
"generic_error": "An error occured",
|
||||
"generic_error_message": "An error occured: {0}",
|
||||
|
|
@ -587,6 +588,9 @@
|
|||
"move_account_error": "Error moving account: {error}",
|
||||
"discoverable": "Allow discovery of this account in search results and other services",
|
||||
"domain_mutes": "Domains",
|
||||
"domain_mutes2": "Excluded domains",
|
||||
"user_mutes2": "Muted users",
|
||||
"user_blocks": "Blocked users",
|
||||
"avatar_size_instruction": "The recommended minimum size for avatar images is 150x150 pixels. Recommended aspect ratio is 1:1",
|
||||
"banner_size_instruction": "The recommended minimum size for banner images is 450x150 pixels. Recommended aspect ratio is 3:1",
|
||||
"pad_emoji": "Pad emoji with spaces when adding from picker",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue