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>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="list"
|
class="List"
|
||||||
role="list"
|
:class="{ '-scrollable': scrollable }"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="item in items"
|
v-if="selectable"
|
||||||
:key="getKey(item)"
|
class="header"
|
||||||
class="list-item"
|
|
||||||
:class="[getClass(item), nonInteractive ? '-non-interactive' : '']"
|
|
||||||
role="listitem"
|
|
||||||
>
|
>
|
||||||
<slot
|
<div class="checkbox-wrapper">
|
||||||
name="item"
|
<Checkbox
|
||||||
:item="item"
|
: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>
|
||||||
<div
|
<div
|
||||||
v-if="items.length === 0 && !!$slots.empty"
|
class="list"
|
||||||
class="list-empty-content faint"
|
role="list"
|
||||||
>
|
>
|
||||||
<slot name="empty" />
|
<div
|
||||||
<slot name="load" />
|
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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script src="./list.js"></script>
|
||||||
export default {
|
|
||||||
props: {
|
<style src="./list.css"></style>
|
||||||
items: {
|
|
||||||
type: Array,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
getKey: {
|
|
||||||
type: Function,
|
|
||||||
default: (item) => item.id,
|
|
||||||
},
|
|
||||||
getClass: {
|
|
||||||
type: Function,
|
|
||||||
default: () => '',
|
|
||||||
},
|
|
||||||
nonInteractive: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,7 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
.list {
|
.list {
|
||||||
flex: 1 1 0;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-item-inner {
|
&-item-inner {
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@
|
||||||
|
|
||||||
.tab-slot-wrapper {
|
.tab-slot-wrapper {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
|
position: relative;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 0 1em;
|
padding: 0 1em;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
@ -56,10 +57,16 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&.-full-height {
|
&.-full-height {
|
||||||
|
height: 100%;
|
||||||
> * {
|
> * {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.-full-width.-full-height {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import { get, map, reject } from 'lodash'
|
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 withSubscription from 'src/components/../hocs/with_subscription/with_subscription'
|
||||||
import Autosuggest from 'src/components/autosuggest/autosuggest.vue'
|
import Autosuggest from 'src/components/autosuggest/autosuggest.vue'
|
||||||
import BlockCard from 'src/components/block_card/block_card.vue'
|
import BlockCard from 'src/components/block_card/block_card.vue'
|
||||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||||
import DomainMuteCard from 'src/components/domain_mute_card/domain_mute_card.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 MuteCard from 'src/components/mute_card/mute_card.vue'
|
||||||
import ProgressButton from 'src/components/progress_button/progress_button.vue'
|
import ProgressButton from 'src/components/progress_button/progress_button.vue'
|
||||||
import SelectableList from 'src/components/selectable_list/selectable_list.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 { useInstanceStore } from 'src/stores/instance.js'
|
||||||
import { useOAuthTokensStore } from 'src/stores/oauth_tokens.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 = {
|
const MutesAndBlocks = {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|
@ -52,12 +26,10 @@ const MutesAndBlocks = {
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
TabSwitcher,
|
TabSwitcher,
|
||||||
BlockList,
|
|
||||||
MuteList,
|
|
||||||
DomainMuteList,
|
|
||||||
BlockCard,
|
|
||||||
MuteCard,
|
|
||||||
DomainMuteCard,
|
DomainMuteCard,
|
||||||
|
BlockCard,
|
||||||
|
List,
|
||||||
|
MuteCard,
|
||||||
ProgressButton,
|
ProgressButton,
|
||||||
Autosuggest,
|
Autosuggest,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
|
|
@ -102,6 +74,15 @@ const MutesAndBlocks = {
|
||||||
})
|
})
|
||||||
.join('\n')
|
.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) {
|
activateTab(tabName) {
|
||||||
this.activeTab = tabName
|
this.activeTab = tabName
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
.mutes-and-blocks-tab {
|
.mutes-and-blocks-tab {
|
||||||
height: 100%;
|
min-height: 100%;
|
||||||
|
|
||||||
.usersearch-wrapper {
|
.usersearch-wrapper {
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
|
|
@ -26,4 +26,12 @@
|
||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
width: 10em;
|
width: 10em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.blocks, .mutes {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,12 @@
|
||||||
<template>
|
<template>
|
||||||
<tab-switcher
|
<tab-switcher
|
||||||
:scrollable-tabs="true"
|
|
||||||
class="mutes-and-blocks-tab"
|
class="mutes-and-blocks-tab"
|
||||||
>
|
:scrollable-tabs="true"
|
||||||
<div :label="$t('settings.blocks_tab')">
|
>
|
||||||
|
<div
|
||||||
|
class="blocks"
|
||||||
|
:label="$t('settings.user_blocks')"
|
||||||
|
>
|
||||||
<div class="usersearch-wrapper">
|
<div class="usersearch-wrapper">
|
||||||
<Autosuggest
|
<Autosuggest
|
||||||
:filter="filterUnblockedUsers"
|
:filter="filterUnblockedUsers"
|
||||||
|
|
@ -17,9 +20,12 @@
|
||||||
</template>
|
</template>
|
||||||
</Autosuggest>
|
</Autosuggest>
|
||||||
</div>
|
</div>
|
||||||
<BlockList
|
<List
|
||||||
:refresh="true"
|
|
||||||
:get-key="i => i"
|
:get-key="i => i"
|
||||||
|
:items-function="getBlocks"
|
||||||
|
:fetch-function="() => $store.dispatch('fetchBlocks')"
|
||||||
|
scrollable
|
||||||
|
selectable
|
||||||
>
|
>
|
||||||
<template #header="{selected}">
|
<template #header="{selected}">
|
||||||
<div class="bulk-actions">
|
<div class="bulk-actions">
|
||||||
|
|
@ -51,103 +57,106 @@
|
||||||
<template #empty>
|
<template #empty>
|
||||||
{{ $t('settings.no_blocks') }}
|
{{ $t('settings.no_blocks') }}
|
||||||
</template>
|
</template>
|
||||||
</BlockList>
|
</List>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div :label="$t('settings.mutes_tab')">
|
<div class="mutes" :label="$t('settings.user_mutes2')">
|
||||||
<tab-switcher>
|
<div class="usersearch-wrapper">
|
||||||
<div :label="$t('settings.user_mutes')">
|
<Autosuggest
|
||||||
<div class="usersearch-wrapper">
|
:filter="filterUnMutedUsers"
|
||||||
<Autosuggest
|
:query="queryUserIds"
|
||||||
:filter="filterUnMutedUsers"
|
:placeholder="$t('settings.search_user_to_mute')"
|
||||||
: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">
|
{{ $t('user_card.mute') }}
|
||||||
<MuteCard
|
<template #progress>
|
||||||
:user-id="row.item"
|
{{ $t('user_card.mute_progress') }}
|
||||||
/>
|
|
||||||
</template>
|
</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>
|
</div>
|
||||||
<MuteList
|
</template>
|
||||||
:refresh="true"
|
<template #item="{item}">
|
||||||
:get-key="i => i"
|
<MuteCard :user-id="item" />
|
||||||
>
|
</template>
|
||||||
<template #header="{selected}">
|
<template #empty>
|
||||||
<div class="bulk-actions">
|
{{ $t('settings.no_mutes') }}
|
||||||
<ProgressButton
|
</template>
|
||||||
v-if="selected.length > 0"
|
</List>
|
||||||
class="btn button-default"
|
</div>
|
||||||
: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>
|
|
||||||
|
|
||||||
<div :label="$t('settings.domain_mutes')">
|
<div :label="$t('settings.domain_mutes2')">
|
||||||
<div class="domain-mute-form">
|
<div class="domain-mute-form">
|
||||||
<Autosuggest
|
<Autosuggest
|
||||||
:filter="filterUnMutedDomains"
|
:filter="filterUnMutedDomains"
|
||||||
:query="queryKnownDomains"
|
:query="queryKnownDomains"
|
||||||
:placeholder="$t('settings.type_domains_to_mute')"
|
: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">
|
{{ $t('domain_mute_card.unmute') }}
|
||||||
<DomainMuteCard
|
<template #progress>
|
||||||
:domain="row.item"
|
{{ $t('domain_mute_card.unmute_progress') }}
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
</Autosuggest>
|
</ProgressButton>
|
||||||
</div>
|
</div>
|
||||||
<DomainMuteList
|
</template>
|
||||||
:refresh="true"
|
<template #item="{item}">
|
||||||
:get-key="i => i"
|
{{ item }}
|
||||||
>
|
<DomainMuteCard :domain="item" />
|
||||||
<template #header="{selected}">
|
</template>
|
||||||
<div class="bulk-actions">
|
<template #empty>
|
||||||
<ProgressButton
|
{{ $t('settings.no_mutes') }}
|
||||||
v-if="selected.length > 0"
|
</template>
|
||||||
class="btn button-default"
|
</List>
|
||||||
: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>
|
|
||||||
</div>
|
</div>
|
||||||
</tab-switcher>
|
</tab-switcher>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@
|
||||||
|
|
||||||
.contents.scrollable-tabs {
|
.contents.scrollable-tabs {
|
||||||
flex-basis: 0;
|
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 TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
|
||||||
import Timeline from 'src/components/timeline/timeline.vue'
|
import Timeline from 'src/components/timeline/timeline.vue'
|
||||||
import UserCard from 'src/components/user_card/user_card.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 { useInstanceStore } from 'src/stores/instance.js'
|
||||||
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
|
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
|
||||||
|
|
@ -19,28 +18,6 @@ import { faCircleNotch } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
library.add(faCircleNotch)
|
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 defaultTabKey = 'statuses'
|
||||||
|
|
||||||
const UserProfile = {
|
const UserProfile = {
|
||||||
|
|
@ -64,6 +41,8 @@ const UserProfile = {
|
||||||
unmounted() {
|
unmounted() {
|
||||||
this.stopFetching()
|
this.stopFetching()
|
||||||
useInterfaceStore().setForeignProfileBackground(null)
|
useInterfaceStore().setForeignProfileBackground(null)
|
||||||
|
this.$store.dispatch('clearFollowers', this.userId)
|
||||||
|
this.$store.dispatch('clearFriends', this.userId)
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
timeline() {
|
timeline() {
|
||||||
|
|
@ -109,6 +88,18 @@ const UserProfile = {
|
||||||
setFooterRef(el) {
|
setFooterRef(el) {
|
||||||
this.footerRef = 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) {
|
load(userNameOrId) {
|
||||||
const startFetchingTimeline = (timeline, userId) => {
|
const startFetchingTimeline = (timeline, userId) => {
|
||||||
// Clear timeline only if load another user's profile
|
// Clear timeline only if load another user's profile
|
||||||
|
|
@ -203,8 +194,7 @@ const UserProfile = {
|
||||||
components: {
|
components: {
|
||||||
UserCard,
|
UserCard,
|
||||||
Timeline,
|
Timeline,
|
||||||
FollowerList,
|
List,
|
||||||
FriendList,
|
|
||||||
FollowCard,
|
FollowCard,
|
||||||
TabSwitcher,
|
TabSwitcher,
|
||||||
Conversation,
|
Conversation,
|
||||||
|
|
|
||||||
|
|
@ -39,14 +39,15 @@
|
||||||
:label="$t('user_card.followees')"
|
:label="$t('user_card.followees')"
|
||||||
:disabled="!user.friends_count"
|
:disabled="!user.friends_count"
|
||||||
>
|
>
|
||||||
<FriendList
|
<List
|
||||||
:user-id="userId"
|
:user-id="userId"
|
||||||
:non-interactive="true"
|
:items-function="getFriends"
|
||||||
|
:fetch-function="() => $store.dispatch('fetchFriends', userId)"
|
||||||
>
|
>
|
||||||
<template #item="{item}">
|
<template #item="{item}">
|
||||||
<FollowCard :user="item" />
|
<FollowCard :user="item" />
|
||||||
</template>
|
</template>
|
||||||
</FriendList>
|
</List>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="followersTabVisible"
|
v-if="followersTabVisible"
|
||||||
|
|
@ -55,9 +56,10 @@
|
||||||
:label="$t('user_card.followers')"
|
:label="$t('user_card.followers')"
|
||||||
:disabled="!user.followers_count"
|
:disabled="!user.followers_count"
|
||||||
>
|
>
|
||||||
<FollowerList
|
<List
|
||||||
:user-id="userId"
|
:user-id="userId"
|
||||||
:non-interactive="true"
|
:items-function="getFollowers"
|
||||||
|
:fetch-function="() => $store.dispatch('fetchFollowers', userId)"
|
||||||
>
|
>
|
||||||
<template #item="{item}">
|
<template #item="{item}">
|
||||||
<FollowCard
|
<FollowCard
|
||||||
|
|
@ -65,7 +67,7 @@
|
||||||
:no-follows-you="isUs"
|
:no-follows-you="isUs"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</FollowerList>
|
</List>
|
||||||
</div>
|
</div>
|
||||||
<Timeline
|
<Timeline
|
||||||
key="media"
|
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",
|
"apply": "Apply",
|
||||||
"submit": "Submit",
|
"submit": "Submit",
|
||||||
"more": "More",
|
"more": "More",
|
||||||
|
"no_more": "No more items",
|
||||||
"loading": "Loading…",
|
"loading": "Loading…",
|
||||||
"generic_error": "An error occured",
|
"generic_error": "An error occured",
|
||||||
"generic_error_message": "An error occured: {0}",
|
"generic_error_message": "An error occured: {0}",
|
||||||
|
|
@ -587,6 +588,9 @@
|
||||||
"move_account_error": "Error moving account: {error}",
|
"move_account_error": "Error moving account: {error}",
|
||||||
"discoverable": "Allow discovery of this account in search results and other services",
|
"discoverable": "Allow discovery of this account in search results and other services",
|
||||||
"domain_mutes": "Domains",
|
"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",
|
"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",
|
"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",
|
"pad_emoji": "Pad emoji with spaces when adding from picker",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue