Skip to content

Commit 4255092

Browse files
committed
feat(MemberList): implement lazy loading
Signed-off-by: Grigory V <scratchx@gmx.com>
1 parent c5ed678 commit 4255092

1 file changed

Lines changed: 47 additions & 23 deletions

File tree

src/components/MemberList/MemberList.vue

Lines changed: 47 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,17 @@
2929
</template>
3030
</NcEmptyContent>
3131

32-
<div v-else class="member-grid">
32+
<VList
33+
v-else
34+
v-slot="{ item }"
35+
class="member-list__virtual"
36+
:style="virtualListStyle"
37+
:data="flatList">
3338
<MemberGridItem
34-
v-for="member in flatList"
35-
:key="`member-grid-item-${member.id}`"
36-
:member="member"
37-
:is-team="!member.isUser" />
38-
</div>
39+
:key="`member-grid-item-${item.id}`"
40+
:member="item"
41+
:is-team="!item.isUser" />
42+
</VList>
3943

4044
<!-- member picker -->
4145
<EntityPicker
@@ -59,6 +63,7 @@ import { showError, showWarning } from '@nextcloud/dialogs'
5963
import { subscribe } from '@nextcloud/event-bus'
6064
import { t } from '@nextcloud/l10n'
6165
import { NcEmptyContent } from '@nextcloud/vue'
66+
import { VList } from 'virtua/vue'
6267
import { defineComponent } from 'vue'
6368
import IconContact from 'vue-material-design-icons/AccountMultipleOutline.vue'
6469
import EntityPicker from '../EntityPicker/EntityPicker.vue'
@@ -76,6 +81,7 @@ export default defineComponent({
7681
IconContact,
7782
MemberGridItem,
7883
NcEmptyContent,
84+
VList,
7985
},
8086
8187
mixins: [IsMobileMixin, RouterMixin],
@@ -103,6 +109,8 @@ export default defineComponent({
103109
pickerData: [],
104110
pickerSelection: {},
105111
pickerTypes: CIRCLES_MEMBER_GROUPING,
112+
113+
circleHeaderHeight: 0,
106114
}
107115
},
108116
@@ -137,14 +145,47 @@ export default defineComponent({
137145
hasMembers() {
138146
return this.flatList.length > 0
139147
},
148+
149+
virtualListStyle() {
150+
const gridBaseline = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--default-grid-baseline')) || 4
151+
const headerHeight = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--header-height')) || 50
152+
const padding = gridBaseline * 32
153+
const availableHeight = window.innerHeight - headerHeight - this.circleHeaderHeight - padding
154+
return {
155+
height: `${Math.max(availableHeight, 200)}px`,
156+
}
157+
},
140158
},
141159
142160
mounted() {
143161
subscribe('contacts:circles:append', this.onShowPicker)
144162
subscribe('guests:user:created', this.onGuestCreated)
163+
this.measureCircleHeader()
164+
},
165+
166+
beforeUnmount() {
167+
this.resizeObserver?.disconnect()
145168
},
146169
147170
methods: {
171+
/**
172+
* Measure the circle details header height from the DOM
173+
* and keep it updated via ResizeObserver.
174+
*/
175+
measureCircleHeader() {
176+
const header = document.querySelector('.circle-details__header-wrapper')
177+
if (!header) {
178+
return
179+
}
180+
this.circleHeaderHeight = header.getBoundingClientRect().height
181+
this.resizeObserver = new ResizeObserver((entries) => {
182+
for (const entry of entries) {
183+
this.circleHeaderHeight = entry.contentRect.height
184+
}
185+
})
186+
this.resizeObserver.observe(header)
187+
},
188+
148189
/**
149190
* Show picker and fetch for recommendations
150191
* Cache the circleId in case the url change or something
@@ -250,30 +291,13 @@ export default defineComponent({
250291

251292
<style lang="scss" scoped>
252293
.member-list {
253-
// Make virtual scroller scrollable
254-
max-height: 100%;
255294
max-width: 900px;
256-
overflow: auto;
257295
258296
:deep(.empty-content) {
259297
margin: auto;
260298
}
261299
}
262300
263-
.member-grid {
264-
display: grid;
265-
grid-template-columns: repeat(2, 1fr);
266-
gap: 8px;
267-
268-
@media (max-width: 768px) {
269-
grid-template-columns: 1fr;
270-
}
271-
272-
@media (min-width: 1200px) {
273-
grid-template-columns: repeat(3, 1fr);
274-
}
275-
}
276-
277301
.empty-content {
278302
height: 100%;
279303
}

0 commit comments

Comments
 (0)