Files
VRCX/src/components/dialogs/UserDialog/UserDialogMutualFriendsTab.vue
2026-03-24 20:13:16 +09:00

172 lines
7.0 KiB
Vue

<template>
<div style="display: flex; align-items: center; justify-content: space-between">
<div style="display: flex; align-items: center">
<Button
class="rounded-full"
variant="ghost"
size="icon-sm"
:disabled="userDialog.isMutualFriendsLoading"
@click="getUserMutualFriends(userDialog.id)">
<Spinner v-if="userDialog.isMutualFriendsLoading" />
<RefreshCw v-else />
</Button>
<span class="inline-flex items-center gap-1 ml-1.5 text-sm">
<Users class="size-3.5 text-muted-foreground" />
{{ t('dialog.user.groups.total_count', { count: userDialog.mutualFriends.length }) }}
</span>
</div>
<div style="display: flex; align-items: center">
<Input v-model="searchQuery" class="h-8 w-40 mr-2" placeholder="Search friends" @click.stop />
<span style="margin-right: 6px">{{ t('dialog.user.groups.sort_by') }}</span>
<Select
:model-value="userDialogMutualFriendSortingKey"
:disabled="userDialog.isMutualFriendsLoading"
@update:modelValue="setUserDialogMutualFriendSortingByKey">
<SelectTrigger size="sm" @click.stop>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem
v-for="(item, key) in userDialogMutualFriendSortingOptions"
:key="String(key)"
:value="String(key)">
{{ t(item.name) }}
</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<ul class="flex flex-wrap items-start" style="margin-top: 8px; overflow: auto; max-height: 250px; min-width: 130px">
<li
v-for="user in filteredMutualFriends"
:key="user.id"
class="box-border flex items-center p-1.5 text-[13px] cursor-pointer w-[167px] hover:rounded-[25px_5px_5px_25px]"
@click="showUserDialog(user.id)">
<div class="relative inline-block flex-none size-9 mr-2.5">
<Avatar class="size-9">
<AvatarImage :src="userImage(user)" class="object-cover" />
<AvatarFallback>
<User class="size-4 text-muted-foreground" />
</AvatarFallback>
</Avatar>
</div>
<div class="flex-1 overflow-hidden">
<span
class="block truncate font-medium leading-[18px]"
:style="{ color: user.$userColour }"
v-text="user.displayName"></span>
</div>
</li>
</ul>
</template>
<script setup>
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Button } from '@/components/ui/button';
import { RefreshCw, User, Users } from 'lucide-vue-next';
import { Spinner } from '@/components/ui/spinner';
import { Input } from '@/components/ui/input';
import { computed, ref, watch } from 'vue';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
import { compareByDisplayName, compareByFriendOrder, compareByLastActiveRef } from '../../../shared/utils';
import { useUserDisplay } from '../../../composables/useUserDisplay';
import { database } from '../../../services/database';
import { processBulk } from '../../../services/request';
import { useOptionKeySelect } from '../../../composables/useOptionKeySelect';
import { useUserStore } from '../../../stores';
import { userDialogMutualFriendSortingOptions } from '../../../shared/constants';
import { userRequest } from '../../../api';
import { showUserDialog } from '../../../coordinators/userCoordinator';
const { t } = useI18n();
const { userImage } = useUserDisplay();
const userStore = useUserStore();
const { userDialog, currentUser } = storeToRefs(userStore);
const { cachedUsers } = userStore;
const { selectedKey: userDialogMutualFriendSortingKey, selectByKey: setUserDialogMutualFriendSortingByKey } =
useOptionKeySelect(
userDialogMutualFriendSortingOptions,
() => userDialog.value.mutualFriendSorting,
setUserDialogMutualFriendSorting
);
const searchQuery = ref('');
const filteredMutualFriends = computed(() => {
const friends = userDialog.value.mutualFriends;
const query = searchQuery.value.trim().toLowerCase();
if (!query) return friends;
return friends.filter((u) => (u.displayName || '').toLowerCase().includes(query));
});
watch(() => userDialog.value.id, () => { searchQuery.value = ''; });
/**
*
* @param sortOrder
*/
async function setUserDialogMutualFriendSorting(sortOrder) {
const D = userDialog.value;
D.mutualFriendSorting = sortOrder;
switch (sortOrder.value) {
case 'alphabetical':
D.mutualFriends.sort(compareByDisplayName);
break;
case 'lastActive':
D.mutualFriends.sort(compareByLastActiveRef);
break;
case 'friendOrder':
D.mutualFriends.sort(compareByFriendOrder);
break;
}
}
/**
*
* @param userId
*/
async function getUserMutualFriends(userId) {
userDialog.value.mutualFriends = [];
if (currentUser.value.hasSharedConnectionsOptOut) {
return;
}
userDialog.value.isMutualFriendsLoading = true;
const params = {
userId,
n: 100,
offset: 0
};
processBulk({
fn: userRequest.getMutualFriends,
N: -1,
params,
handle: (args) => {
for (const json of args.json) {
if (userDialog.value.mutualFriends.some((u) => u.id === json.id)) {
continue;
}
const ref = cachedUsers.get(json.id);
if (typeof ref !== 'undefined') {
userDialog.value.mutualFriends.push(ref);
} else {
userDialog.value.mutualFriends.push(json);
}
}
setUserDialogMutualFriendSorting(userDialog.value.mutualFriendSorting);
},
done: (success) => {
userDialog.value.isMutualFriendsLoading = false;
if (success) {
const mutualIds = userDialog.value.mutualFriends.map((u) => u.id);
database.updateMutualsForFriend(userId, mutualIds);
}
}
});
}
defineExpose({ getUserMutualFriends });
</script>