Files
VRCX/src/components/dialogs/InviteDialog/InviteDialog.vue
2026-03-14 22:41:38 +09:00

325 lines
12 KiB
Vue

<template>
<Dialog
:open="inviteDialog.visible"
@update:open="
(open) => {
if (!open) closeInviteDialog();
}
">
<DialogContent class="x-dialog sm:max-w-125">
<DialogHeader>
<DialogTitle>{{ t('dialog.invite.header') }}</DialogTitle>
</DialogHeader>
<div v-if="inviteDialog.visible" class="overflow-hidden">
<Location :location="inviteDialog.worldId" :link="false" class="cursor-default" />
<br />
<Button size="sm" class="mr-2 mt-2" variant="outline" @click="addSelfToInvite">{{
t('dialog.invite.add_self')
}}</Button>
<Button
size="sm"
class="mr-2 mt-2"
variant="outline"
:disabled="inviteDialog.friendsInInstance.length === 0"
@click="addFriendsInInstanceToInvite"
>{{ t('dialog.invite.add_friends_in_instance') }}</Button
>
<Button
class="mt-2"
size="sm"
variant="outline"
:disabled="vipFriends.length === 0"
@click="addFavoriteFriendsToInvite"
>{{ t('dialog.invite.add_favorite_friends') }}</Button
>
<div class="mt-4" style="width: 100%">
<VirtualCombobox
:model-value="Array.isArray(inviteDialog.userIds) ? inviteDialog.userIds : []"
@update:modelValue="setInviteUserIds"
:groups="userPickerGroups"
multiple
:disabled="inviteDialog.loading"
:placeholder="t('dialog.invite.select_placeholder')"
:search-placeholder="t('dialog.invite.select_placeholder')"
:clearable="true">
<template #item="{ item, selected }">
<div class="flex w-full items-center p-1.5 text-[13px]">
<template v-if="item.user">
<div
class="relative inline-block flex-none size-9 mr-2.5"
:class="userStatusClass(item.user)">
<img
class="size-full rounded-full object-cover"
:src="userImage(item.user)"
loading="lazy" />
</div>
<div class="flex-1 overflow-hidden">
<span
class="block truncate font-medium leading-[18px]"
:style="{ color: item.user.$userColour }"
>{{ item.user.displayName }}</span
>
</div>
</template>
<template v-else>
<span>{{ item.label }}</span>
</template>
<CheckIcon :class="['ml-auto size-4', selected ? 'opacity-100' : 'opacity-0']" />
</div>
</template>
</VirtualCombobox>
</div>
</div>
<DialogFooter>
<Button
variant="secondary"
class="mr-2"
:disabled="inviteDialog.loading || !inviteDialog.userIds.length"
@click="showSendInviteDialog"
>{{ t('dialog.invite.invite_with_message') }}</Button
>
<Button :disabled="inviteDialog.loading || !inviteDialog.userIds.length" @click="sendInvite">{{
t('dialog.invite.invite')
}}</Button>
</DialogFooter>
</DialogContent>
<SendInviteDialog
v-model:sendInviteDialogVisible="sendInviteDialogVisible"
v-model:sendInviteDialog="sendInviteDialog"
:invite-dialog="inviteDialog"
@closeInviteDialog="closeInviteDialog" />
</Dialog>
</template>
<script setup>
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { computed, ref } from 'vue';
import { Button } from '@/components/ui/button';
import { Check as CheckIcon } from 'lucide-vue-next';
import { storeToRefs } from 'pinia';
import { toast } from 'vue-sonner';
import { useI18n } from 'vue-i18n';
import { useFriendStore, useGalleryStore, useInviteStore, useModalStore, useUserStore } from '../../../stores';
import { parseLocation } from '../../../shared/utils';
import { useUserDisplay } from '../../../composables/useUserDisplay';
import { instanceRequest, notificationRequest } from '../../../api';
import { VirtualCombobox } from '../../ui/virtual-combobox';
import SendInviteDialog from './SendInviteDialog.vue';
const { userImage, userStatusClass } = useUserDisplay();
const { vipFriends, onlineFriends, activeFriends } = storeToRefs(useFriendStore());
const { refreshInviteMessageTableData } = useInviteStore();
const { currentUser } = storeToRefs(useUserStore());
const { clearInviteImageUpload } = useGalleryStore();
const modalStore = useModalStore();
const { t } = useI18n();
const props = defineProps({
inviteDialog: {
type: Object,
required: true
}
});
const emit = defineEmits(['closeInviteDialog']);
const sendInviteDialogVisible = ref(false);
const sendInviteDialog = ref({
messageSlot: {},
userId: '',
params: {}
});
const userPickerGroups = computed(() => {
const groups = [];
if (currentUser.value) {
groups.push({
key: 'me',
label: t('side_panel.me'),
items: [
{
value: String(currentUser.value.id),
label: currentUser.value.displayName,
search: currentUser.value.displayName,
user: currentUser.value
}
]
});
}
const addFriendGroup = (key, label, friends) => {
if (!friends?.length) return;
groups.push({
key,
label,
items: friends.map((friend) => {
const user = friend?.ref ?? null;
const displayName = resolveUserDisplayName(friend.id);
return {
value: String(friend.id),
label: displayName,
search: displayName,
user
};
})
});
};
addFriendGroup('friendsInInstance', t('dialog.invite.friends_in_instance'), props.inviteDialog?.friendsInInstance);
addFriendGroup('vip', t('side_panel.favorite'), vipFriends.value);
addFriendGroup('online', t('side_panel.online'), onlineFriends.value);
addFriendGroup('active', t('side_panel.active'), activeFriends.value);
return groups;
});
/**
*
* @param value
*/
function setInviteUserIds(value) {
const next = Array.isArray(value) ? value.map((v) => String(v ?? '')).filter(Boolean) : [];
const ids = Array.isArray(props.inviteDialog.userIds) ? props.inviteDialog.userIds : [];
ids.splice(0, ids.length, ...next);
}
const friendById = computed(() => {
const map = new Map();
for (const friend of props.inviteDialog?.friendsInInstance ?? []) map.set(friend.id, friend);
for (const friend of vipFriends.value) map.set(friend.id, friend);
for (const friend of onlineFriends.value) map.set(friend.id, friend);
for (const friend of activeFriends.value) map.set(friend.id, friend);
return map;
});
/**
*
* @param userId
*/
function resolveUserDisplayName(userId) {
if (currentUser.value?.id && currentUser.value.id === userId) {
return currentUser.value.displayName;
}
const friend = friendById.value.get(userId);
return friend?.ref?.displayName ?? friend?.name ?? String(userId);
}
/**
*
*/
function closeInviteDialog() {
emit('closeInviteDialog');
}
/**
*
* @param params
* @param userId
*/
function showSendInviteDialog(params, userId) {
sendInviteDialog.value = {
params,
userId,
messageSlot: {}
};
refreshInviteMessageTableData('message');
clearInviteImageUpload();
sendInviteDialogVisible.value = true;
}
/**
*
*/
function addSelfToInvite() {
const D = props.inviteDialog;
if (!D.userIds.includes(currentUser.value.id)) {
D.userIds.push(currentUser.value.id);
}
}
/**
*
*/
function addFriendsInInstanceToInvite() {
const D = props.inviteDialog;
for (const friend of D.friendsInInstance) {
if (!D.userIds.includes(friend.id)) {
D.userIds.push(friend.id);
}
}
}
/**
*
*/
function addFavoriteFriendsToInvite() {
const D = props.inviteDialog;
for (const friend of vipFriends.value) {
if (!D.userIds.includes(friend.id)) {
D.userIds.push(friend.id);
}
}
}
/**
*
*/
function sendInvite() {
modalStore
.confirm({
description: t('confirm.invite'),
title: 'Confirm'
})
.then(({ ok }) => {
if (!ok) return;
const D = props.inviteDialog;
if (D.loading === true) {
return;
}
D.loading = true;
const inviteLoop = () => {
if (D.userIds.length > 0) {
const receiverUserId = D.userIds.shift();
if (receiverUserId === currentUser.value.id) {
// can't invite self!?
const L = parseLocation(D.worldId);
instanceRequest
.selfInvite({
instanceId: L.instanceId,
worldId: L.worldId
})
.finally(inviteLoop);
} else {
notificationRequest
.sendInvite(
{
instanceId: D.worldId,
worldId: D.worldId,
worldName: D.worldName
},
receiverUserId
)
.finally(inviteLoop);
}
} else {
D.loading = false;
D.visible = false;
toast.success(t('message.invite.sent'));
}
};
inviteLoop();
});
}
</script>
}) .catch(() => {});