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

470 lines
15 KiB
Vue

<template>
<div class="w-223">
<DialogHeader class="sr-only">
<DialogTitle>{{
userDialog.ref?.displayName || userDialog.id || t('dialog.user.info.header')
}}</DialogTitle>
<DialogDescription>{{ getUserStateText(userDialog.ref || {}) }}</DialogDescription>
</DialogHeader>
<UserSummaryHeader
:get-user-state-text="getUserStateText"
:copy-user-display-name="copyUserDisplayName"
:toggle-badge-visibility="toggleBadgeVisibility"
:toggle-badge-showcased="toggleBadgeShowcased"
:user-dialog-command="userDialogCommand" />
<TabsUnderline
v-model="userDialog.activeTab"
:items="userDialogTabs"
:unmount-on-hide="false"
@update:modelValue="userDialogTabClick">
<template #Info>
<UserDialogInfoTab ref="infoTabRef" @show-bio-dialog="showBioDialog" />
</template>
<template v-if="userDialog.id !== currentUser.id && !currentUser.hasSharedConnectionsOptOut" #mutual>
<UserDialogMutualFriendsTab ref="mutualFriendsTabRef" />
</template>
<template #Groups>
<UserDialogGroupsTab ref="groupsTabRef" />
</template>
<template #Worlds>
<UserDialogWorldsTab ref="worldsTabRef" />
</template>
<template #favorite-worlds>
<UserDialogFavoriteWorldsTab ref="favoriteWorldsTabRef" />
</template>
<template #Avatars>
<UserDialogAvatarsTab ref="avatarsTabRef" />
</template>
<template #JSON>
<DialogJsonTab
:tree-data="treeData"
:tree-data-key="treeData?.id"
:dialog-id="userDialog.id"
:dialog-ref="userDialog.ref"
@refresh="refreshUserDialogTreeData()" />
</template>
</TabsUnderline>
<SendInviteDialog
v-model:sendInviteDialogVisible="sendInviteDialogVisible"
v-model:sendInviteDialog="sendInviteDialog"
@closeInviteDialog="closeInviteDialog" />
<SendInviteRequestDialog
v-model:sendInviteRequestDialogVisible="sendInviteRequestDialogVisible"
v-model:sendInviteDialog="sendInviteDialog"
@closeInviteDialog="closeInviteDialog" />
<SocialStatusDialog
:social-status-dialog="socialStatusDialog"
:social-status-history-table="socialStatusHistoryTable" />
<LanguageDialog />
<BioDialog :bio-dialog="bioDialog" />
<PronounsDialog :pronouns-dialog="pronounsDialog" />
<ModerateGroupDialog />
</div>
</template>
<script setup>
import { computed, defineAsyncComponent, ref, watch } from 'vue';
import { DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { TabsUnderline } from '@/components/ui/tabs';
import { storeToRefs } from 'pinia';
import { toast } from 'vue-sonner';
import { useI18n } from 'vue-i18n';
import {
useAvatarStore,
useFavoriteStore,
useFriendStore,
useGalleryStore,
useGroupStore,
useInstanceStore,
useInviteStore,
useLocationStore,
useModalStore,
useModerationStore,
useNotificationStore,
useUserStore
} from '../../../stores';
import { copyToClipboard } from '../../../shared/utils';
import { formatJsonVars } from '../../../shared/utils/base/ui';
import { miscRequest } from '../../../api';
import { useUserDialogCommands } from './useUserDialogCommands';
import DialogJsonTab from '../DialogJsonTab.vue';
import SendInviteDialog from '../InviteDialog/SendInviteDialog.vue';
import UserDialogAvatarsTab from './UserDialogAvatarsTab.vue';
import UserDialogFavoriteWorldsTab from './UserDialogFavoriteWorldsTab.vue';
import UserDialogGroupsTab from './UserDialogGroupsTab.vue';
import UserDialogInfoTab from './UserDialogInfoTab.vue';
import UserDialogMutualFriendsTab from './UserDialogMutualFriendsTab.vue';
import UserDialogWorldsTab from './UserDialogWorldsTab.vue';
import UserSummaryHeader from './UserSummaryHeader.vue';
const BioDialog = defineAsyncComponent(() => import('./BioDialog.vue'));
const LanguageDialog = defineAsyncComponent(() => import('./LanguageDialog.vue'));
const PronounsDialog = defineAsyncComponent(() => import('./PronounsDialog.vue'));
const SendInviteRequestDialog = defineAsyncComponent(() => import('./SendInviteRequestDialog.vue'));
const SocialStatusDialog = defineAsyncComponent(() => import('./SocialStatusDialog.vue'));
const ModerateGroupDialog = defineAsyncComponent(() => import('../ModerateGroupDialog.vue'));
const { t } = useI18n();
const userDialogTabs = computed(() => {
const tabs = [
{ value: 'Info', label: t('dialog.user.info.header') },
{ value: 'Groups', label: t('dialog.user.groups.header') },
{ value: 'Worlds', label: t('dialog.user.worlds.header') },
{ value: 'favorite-worlds', label: t('dialog.user.favorite_worlds.header') },
{ value: 'Avatars', label: t('dialog.user.avatars.header') },
{ value: 'JSON', label: t('dialog.user.json.header') }
];
if (userDialog.value.id !== currentUser.value.id && !currentUser.value.hasSharedConnectionsOptOut) {
tabs.splice(1, 0, { value: 'mutual', label: t('dialog.user.mutual_friends.header') });
}
return tabs;
});
const infoTabRef = ref(null);
const favoriteWorldsTabRef = ref(null);
const mutualFriendsTabRef = ref(null);
const worldsTabRef = ref(null);
const avatarsTabRef = ref(null);
const groupsTabRef = ref(null);
const modalStore = useModalStore();
const instanceStore = useInstanceStore();
const { userDialog, languageDialog, currentUser, isLocalUserVrcPlusSupporter } = storeToRefs(useUserStore());
const { cachedUsers, showUserDialog, refreshUserDialogAvatars, showSendBoopDialog } = useUserStore();
const { showFavoriteDialog } = useFavoriteStore();
const { showAvatarDialog, showAvatarAuthorDialog } = useAvatarStore();
const { showGroupDialog, showModerateGroupDialog } = useGroupStore();
const { inviteGroupDialog } = storeToRefs(useGroupStore());
const { lastLocation, lastLocationDestination } = storeToRefs(useLocationStore());
const { refreshInviteMessageTableData } = useInviteStore();
const { friendLogTable } = storeToRefs(useFriendStore());
const { getFriendRequest, handleFriendDelete } = useFriendStore();
const { clearInviteImageUpload, showGalleryPage } = useGalleryStore();
const { applyPlayerModeration, handlePlayerModerationDelete } = useModerationStore();
const {
sendInviteDialogVisible,
sendInviteDialog,
sendInviteRequestDialogVisible,
userDialogCommand,
registerCallbacks
} = useUserDialogCommands(userDialog, {
t,
toast,
modalStore,
currentUser,
cachedUsers,
friendLogTable,
lastLocation,
lastLocationDestination,
inviteGroupDialog,
showUserDialog,
showFavoriteDialog,
showAvatarDialog,
showAvatarAuthorDialog,
showModerateGroupDialog,
showSendBoopDialog,
showGalleryPage,
getFriendRequest,
handleFriendDelete,
applyPlayerModeration,
handlePlayerModerationDelete,
refreshInviteMessageTableData,
clearInviteImageUpload,
instanceStore,
useNotificationStore
});
watch(
() => userDialog.value.loading,
() => {
if (userDialog.value.visible) {
!userDialog.value.loading && loadLastActiveTab();
}
}
);
const userDialogLastMutualFriends = ref('');
const userDialogLastGroup = ref('');
const userDialogLastAvatar = ref('');
const userDialogLastWorld = ref('');
const userDialogLastFavoriteWorld = ref('');
const socialStatusDialog = ref({
visible: false,
loading: false,
status: '',
statusDescription: ''
});
const socialStatusHistoryTable = ref({
data: [],
layout: 'table'
});
const bioDialog = ref({
visible: false,
loading: false,
bio: '',
bioLinks: []
});
const pronounsDialog = ref({
visible: false,
loading: false,
pronouns: ''
});
const treeData = ref({});
/**
*
* @param user
*/
function getUserStateText(user) {
let state = '';
if (user.state === 'active') {
state = t('dialog.user.status.active');
} else if (user.state === 'offline') {
state = t('dialog.user.status.offline');
} else {
return getUserStatusText(user.status);
}
if (user.status && user.status !== 'active') {
state += ` (${getUserStatusText(user.status)})`;
}
return state;
}
/**
*
* @param status
*/
function getUserStatusText(status) {
if (status === 'active') {
return t('dialog.user.status.active');
}
if (status === 'join me') {
return t('dialog.user.status.join_me');
}
if (status === 'ask me') {
return t('dialog.user.status.ask_me');
}
if (status === 'busy') {
return t('dialog.user.status.busy');
}
return t('dialog.user.status.offline');
}
/**
*
*/
function refreshUserDialogTreeData() {
const D = userDialog.value;
if (D.id === currentUser.value.id) {
treeData.value = formatJsonVars({
...currentUser.value,
...D.ref
});
return;
}
treeData.value = formatJsonVars(D.ref);
}
/**
*
* @param tabName
*/
function handleUserDialogTab(tabName) {
userDialog.value.lastActiveTab = tabName;
const userId = userDialog.value.id;
if (tabName === 'Info') {
infoTabRef.value?.onTabActivated();
} else if (tabName === 'mutual') {
if (userId === currentUser.value.id) {
userDialog.value.activeTab = 'Info';
userDialog.value.lastActiveTab = 'Info';
return;
}
if (userDialogLastMutualFriends.value !== userId) {
userDialogLastMutualFriends.value = userId;
mutualFriendsTabRef.value?.getUserMutualFriends(userId);
}
} else if (tabName === 'Groups') {
if (userDialogLastGroup.value !== userId) {
userDialogLastGroup.value = userId;
groupsTabRef.value?.getUserGroups(userId);
}
} else if (tabName === 'Avatars') {
avatarsTabRef.value?.setUserDialogAvatars(userId);
if (userDialogLastAvatar.value !== userId) {
userDialogLastAvatar.value = userId;
if (userId === currentUser.value.id) {
refreshUserDialogAvatars();
} else {
avatarsTabRef.value?.setUserDialogAvatarsRemote(userId);
}
}
} else if (tabName === 'Worlds') {
worldsTabRef.value?.setUserDialogWorlds(userId);
if (userDialogLastWorld.value !== userId) {
userDialogLastWorld.value = userId;
worldsTabRef.value?.refreshUserDialogWorlds();
}
} else if (tabName === 'favorite-worlds') {
if (userDialogLastFavoriteWorld.value !== userId) {
userDialogLastFavoriteWorld.value = userId;
favoriteWorldsTabRef.value?.getUserFavoriteWorlds(userId);
}
} else if (tabName === 'JSON') {
refreshUserDialogTreeData();
}
}
/**
*
*/
function loadLastActiveTab() {
handleUserDialogTab(userDialog.value.lastActiveTab);
}
/**
*
* @param tabName
*/
function userDialogTabClick(tabName) {
if (tabName === userDialog.value.lastActiveTab) {
if (tabName === 'JSON') {
refreshUserDialogTreeData();
}
return;
}
handleUserDialogTab(tabName);
}
/**
*
*/
function showPronounsDialog() {
const D = pronounsDialog.value;
D.pronouns = currentUser.value.pronouns;
D.visible = true;
}
/**
*
*/
function showLanguageDialog() {
const D = languageDialog.value;
D.visible = true;
}
// Register simple dialog openers as callbacks for the command composable
registerCallbacks({
showSocialStatusDialog,
showLanguageDialog,
showBioDialog,
showPronounsDialog,
showEditNoteAndMemoDialog: () => {
infoTabRef.value?.showEditNoteAndMemoDialog();
}
});
/**
*
*/
function showSocialStatusDialog() {
const D = socialStatusDialog.value;
const { statusHistory } = currentUser.value;
const statusHistoryArray = [];
for (let i = 0; i < statusHistory.length; ++i) {
const addStatus = {
no: i + 1,
status: statusHistory[i]
};
statusHistoryArray.push(addStatus);
}
socialStatusHistoryTable.value.data = statusHistoryArray;
D.status = currentUser.value.status;
D.statusDescription = currentUser.value.statusDescription;
D.visible = true;
}
/**
*
* @param badge
*/
async function toggleBadgeVisibility(badge) {
if (badge.hidden) {
badge.showcased = false;
}
const args = await miscRequest.updateBadge({
badgeId: badge.badgeId,
hidden: badge.hidden,
showcased: badge.showcased
});
handleBadgeUpdate(args);
}
/**
*
* @param badge
*/
async function toggleBadgeShowcased(badge) {
if (badge.showcased) {
badge.hidden = false;
}
const args = await miscRequest.updateBadge({
badgeId: badge.badgeId,
hidden: badge.hidden,
showcased: badge.showcased
});
handleBadgeUpdate(args);
}
/**
*
* @param args
*/
function handleBadgeUpdate(args) {
if (args.json) {
toast.success(t('message.badge.updated'));
}
}
/**
*
*/
function showBioDialog() {
const D = bioDialog.value;
D.bio = currentUser.value.bio;
D.bioLinks = currentUser.value.bioLinks.slice();
D.visible = true;
}
/**
*
* @param displayName
*/
function copyUserDisplayName(displayName) {
copyToClipboard(displayName, 'User DisplayName copied to clipboard');
}
/**
*
*/
function closeInviteDialog() {
clearInviteImageUpload();
}
</script>