-
Notifications
You must be signed in to change notification settings - Fork 0
feat: room members list sidebar #50
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
1ea37d0
ccce857
2941f06
5910e65
d34de41
bddb244
5c91b8d
1fa1d3d
e6188f5
18f17c1
07f981c
dcc32be
5b30bb4
91ff27f
cb4ba1a
86fa62b
e5b8f3d
4c7c7df
70b55ed
5d05c39
6adf612
4663890
84d31b9
9c99fe0
2d8756c
9743a4d
25c620f
57c6530
c6e009e
6678542
e16b58c
16c0791
1b96fb2
b94cfdd
64d8269
211bf43
f363ced
434b005
48daa91
a933220
a76d25f
35ac489
f29c7ab
a7a6e84
afa424b
4200b01
3321972
feebe2b
4394ed8
f185211
2a2a038
cf51450
9694a09
7888bdc
2e25d8f
34270ae
3bf300d
fb84df1
f30205a
676afeb
6654353
0c11265
557ea1b
1aac754
c8c4df5
829b28c
894171d
bd86be6
c0e26b7
1cfc4b5
e2455f3
d6f3f0f
516ea25
8ff964e
e5aec62
bfd0a6b
0f73a33
2b88cee
ce1ee4a
be9f6ea
7c55251
2d30c16
2e6f3d5
77ec296
e8b2321
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| <script lang="ts" setup> | ||
| import { VList } from 'virtua/vue' | ||
|
|
||
| const currentRoom = useCurrentRoom() | ||
|
|
||
| const { isLoaded, members } = useRoomMembers(currentRoom) | ||
| const membersGrouped = useRoomMemberGrouping(members, () => currentRoom.value?.roomId) | ||
|
|
||
| const listRef = useTemplateRef('list') | ||
| watch(() => currentRoom.value?.roomId, () => listRef.value?.scrollTo(0)) | ||
| </script> | ||
|
|
||
| <template> | ||
| <div class="border-l border-border shrink-0 h-full w-72"> | ||
| <VList | ||
| v-if="membersGrouped && isLoaded" | ||
| :key="currentRoom?.roomId" | ||
| v-slot="{ item }" | ||
| ref="list" | ||
| :item-size="40" | ||
| :data="membersGrouped.members" | ||
| class="px-2 py-1" | ||
| @vue:mounted="console.log" | ||
| > | ||
| <PageRoomMembersListHeader | ||
| v-if="'type' in item && item.type === 'header'" | ||
| :key="item.title" | ||
| :title="item.title" | ||
| :total="membersGrouped.groupTotals[item.title]" | ||
| /> | ||
|
|
||
| <PageRoomMembersListCard | ||
| v-else | ||
| :key="item.userId" | ||
| :is-owner="item.powerLevel >= 100" | ||
| :user-id="item.userId" | ||
| /> | ||
| </VList> | ||
|
|
||
| <div v-else class="p-2 h-full relative"> | ||
| <!-- <div class="size-full inset-0 absolute z-1 from-transparent to-card to-80% bg-gradient-to-b" /> --> | ||
|
|
||
| <!-- <div class="h-10" /> --> | ||
| <USkeleton | ||
| v-for="item in 8" | ||
| :key="item" | ||
| class="mb-3 h-10 w-full" | ||
| /> | ||
| </div> | ||
| </div> | ||
| </template> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| <script lang="ts" setup> | ||
| const props = defineProps<{ | ||
| userId: string | ||
| }>() | ||
|
|
||
| const profile = useUserProfile(props.userId) | ||
| const creator = useCurrentRoomCreator() | ||
|
|
||
| const triggerRef = useTemplateRef('trigger') | ||
| onMounted(() => { | ||
| const btn = unrefElement(triggerRef) | ||
| if (btn) { | ||
| // workaround to prevent virtual list from scrolling when closing popover | ||
| const nativeFocus = btn.focus.bind(btn) | ||
| btn.focus = (options?: FocusOptions) => nativeFocus({ ...options, preventScroll: true }) | ||
| } | ||
| }) | ||
| </script> | ||
|
|
||
| <template> | ||
| <UProfilePopoverTrigger | ||
| freeze-reference | ||
| :user="userId" | ||
| :content-props="{ | ||
| side: 'left', | ||
| align: 'start', | ||
| collisionPadding: 12, | ||
| sideOffset: 22, | ||
| disableUpdateOnLayoutShift: true, | ||
| }" | ||
| as-child | ||
| > | ||
| <UButton | ||
| ref="trigger" | ||
| class="text-foreground font-normal gap-2 h-10 w-full justify-start data-[popover-open]:bg-muted/75" | ||
| variant="ghost" | ||
| > | ||
| <MatrixAvatar :user="userId" class="shrink-0 size-6" /> | ||
|
|
||
| <p class="shrink-0 truncate"> | ||
| {{ profile?.displayname }} | ||
| </p> | ||
|
|
||
| <UTooltipRoot> | ||
| <UTooltipTrigger as-child> | ||
| <Icon | ||
| v-if="userId === creator" | ||
| name="tabler:crown" | ||
| class="text-primary" | ||
| /> | ||
| </UTooltipTrigger> | ||
| <UTooltipContent> | ||
| Owner | ||
| </UTooltipContent> | ||
| </UTooltipRoot> | ||
| </UButton> | ||
| </UProfilePopoverTrigger> | ||
| </template> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| <script lang="ts" setup> | ||
| defineProps<{ | ||
| title: PowerLevelName | ||
| total: number | ||
| }>() | ||
| </script> | ||
|
|
||
| <template> | ||
| <div class="text-xs text-muted-foreground font-medium px-2 pb-0.5 flex h-8 w-full items-end"> | ||
| <p>{{ upperFirst(title) }} — {{ total }}</p> | ||
| </div> | ||
| </template> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| export function useCurrentRoomCreator() { | ||
| const currentRoom = useCurrentRoom() | ||
|
|
||
| return computed(() => currentRoom.value?.getCreator()) | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,86 @@ | ||||||||||||
| import type { Room, RoomMember } from 'matrix-js-sdk' | ||||||||||||
| import { toRef } from '@vueuse/core' | ||||||||||||
| import QuickLRU from 'quick-lru' | ||||||||||||
|
|
||||||||||||
| const POWER_LEVEL_ORDER = ['admin', 'moderator', 'member'] satisfies PowerLevelName[] | ||||||||||||
|
|
||||||||||||
| type Member = Pick<RoomMember, 'powerLevel' | 'rawDisplayName' | 'membership' | 'name' | 'userId'> & { type: 'member' } | ||||||||||||
| interface MemberHeader { type: 'header', title: PowerLevelName } | ||||||||||||
| interface MemberCachePayload { | ||||||||||||
| members: Prettify<Member | MemberHeader>[] | ||||||||||||
| groupTotals: Record<PowerLevelName, number> | ||||||||||||
| memberCount: number | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| export type GroupedMemberListItem = Prettify<Member | MemberHeader> | ||||||||||||
|
|
||||||||||||
| const roomMemberGroupCache = new QuickLRU<Room['roomId'], MemberCachePayload>({ maxSize: 12 }) | ||||||||||||
|
|
||||||||||||
| export function useRoomMemberGrouping(members: MaybeRefOrGetter<RoomMember[] | undefined>, maybeRoomOrId: MaybeRefOrGetter<MaybeRoomOrId | undefined>, force?: MaybeRefOrGetter<boolean>) { | ||||||||||||
| const membersRef = toRef(members) | ||||||||||||
| const roomIdRef = toRef(maybeRoomOrId) | ||||||||||||
| const forceRef = toRef(force) | ||||||||||||
|
|
||||||||||||
| const membersGrouped = shallowRef<MemberCachePayload>() | ||||||||||||
| const roomId = computed(() => roomIdRef.value ? resolveRoomId(roomIdRef.value) : undefined) | ||||||||||||
|
|
||||||||||||
| watch(membersRef, (members) => { | ||||||||||||
| if (!members?.length || !roomId.value) | ||||||||||||
| return | ||||||||||||
|
|
||||||||||||
| const cached = roomMemberGroupCache.get(roomId.value) | ||||||||||||
| if (cached && !forceRef.value && cached.memberCount === members.length) | ||||||||||||
| return membersGrouped.value = cached | ||||||||||||
|
|
||||||||||||
| const payload = { ...createMembersList(members), memberCount: members.length } | ||||||||||||
| roomMemberGroupCache.set(roomId.value, payload) | ||||||||||||
|
|
||||||||||||
| membersGrouped.value = payload | ||||||||||||
| }, { immediate: true }) | ||||||||||||
|
greptile-apps[bot] marked this conversation as resolved.
|
||||||||||||
|
|
||||||||||||
| return membersGrouped | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| function createMembersList(members: RoomMember[]) { | ||||||||||||
| members.sort((a, b) => { | ||||||||||||
|
Comment on lines
+44
to
+45
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||
| if (a.powerLevel === b.powerLevel) { | ||||||||||||
| const aName = resolveUserName(a) | ||||||||||||
| const bName = resolveUserName(b) | ||||||||||||
|
|
||||||||||||
| return aName.localeCompare(bName) | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| return a.powerLevel > b.powerLevel ? -1 : 1 | ||||||||||||
| }) | ||||||||||||
|
|
||||||||||||
| const membersGrouped = groupBy(members, m => getPowerLevelName(m.powerLevel, true)) | ||||||||||||
| const membersWithHeaders: Prettify<Member | MemberHeader>[] = [] | ||||||||||||
| const groupTotals: MemberCachePayload['groupTotals'] = { | ||||||||||||
| admin: 0, | ||||||||||||
| member: 0, | ||||||||||||
| moderator: 0, | ||||||||||||
| owner: 0, | ||||||||||||
| unknown: 0, | ||||||||||||
| } | ||||||||||||
| for (const groupName of POWER_LEVEL_ORDER) { | ||||||||||||
| const group = membersGrouped[groupName] | ||||||||||||
| if (!group?.length) | ||||||||||||
| continue | ||||||||||||
|
|
||||||||||||
| groupTotals[groupName] = group.length | ||||||||||||
|
|
||||||||||||
| membersWithHeaders.push({ title: groupName, type: 'header' }) | ||||||||||||
| for (let i = 0; i < group.length; i++) { | ||||||||||||
| const member = group[i] | ||||||||||||
| if (!member) | ||||||||||||
| continue | ||||||||||||
|
|
||||||||||||
| membersWithHeaders.push({ type: 'member', ...member }) | ||||||||||||
| } | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| return { | ||||||||||||
| groupTotals, | ||||||||||||
| members: membersWithHeaders, | ||||||||||||
| } | ||||||||||||
| } | ||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| export function useRoomMemberPowerLevel(roomId: MaybeRefOrGetter<MaybeRoomOrId | undefined>, maybeUserOrId: MaybeRefOrGetter<MaybeUserOrId | undefined>) { | ||
| const roomMember = useRoomMember(roomId, maybeUserOrId) | ||
|
|
||
| return computed(() => roomMember.value?.powerLevel) | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.