mirror of
https://github.com/vrcx-team/VRCX.git
synced 2026-04-06 00:32:02 +02:00
refactor
This commit is contained in:
@@ -226,9 +226,7 @@ describe('StatusBar.vue - Servers indicator', () => {
|
||||
test('shows Servers indicator with green dot when no issues', () => {
|
||||
const wrapper = mountStatusBar();
|
||||
expect(wrapper.text()).toContain('Servers');
|
||||
const serversDots = wrapper.findAll(
|
||||
'.bg-\\[var\\(--status-online\\)\\]'
|
||||
);
|
||||
const serversDots = wrapper.findAll('.bg-status-online');
|
||||
expect(serversDots.length).toBeGreaterThan(0);
|
||||
expect(wrapper.find('.bg-\\[\\#e6a23c\\]').exists()).toBe(false);
|
||||
});
|
||||
|
||||
@@ -6,10 +6,11 @@ import { mount } from '@vue/test-utils';
|
||||
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: (key, params) => (params ? `${key}:${JSON.stringify(params)}` : key)
|
||||
t: (key, params) => (params ? `${key}:${JSON.stringify(params)}` : key),
|
||||
locale: require('vue').ref('en')
|
||||
}),
|
||||
createI18n: () => ({
|
||||
global: { t: (key) => key },
|
||||
global: { t: (key) => key , locale: require('vue').ref('en') },
|
||||
install: vi.fn()
|
||||
})
|
||||
}));
|
||||
|
||||
@@ -4,15 +4,20 @@ import { mount } from '@vue/test-utils';
|
||||
|
||||
// ─── Mocks ───────────────────────────────────────────────────────────
|
||||
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: (key, params) => (params ? `${key}:${JSON.stringify(params)}` : key)
|
||||
}),
|
||||
createI18n: () => ({
|
||||
global: { t: (key) => key },
|
||||
install: vi.fn()
|
||||
})
|
||||
}));
|
||||
vi.mock('vue-i18n', () => {
|
||||
const { ref } = require('vue');
|
||||
return {
|
||||
useI18n: () => ({
|
||||
t: (key, params) =>
|
||||
params ? `${key}:${JSON.stringify(params)}` : key,
|
||||
locale: ref('en')
|
||||
}),
|
||||
createI18n: () => ({
|
||||
global: { t: (key) => key, locale: ref('en') },
|
||||
install: vi.fn()
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('../../../../plugin/router', () => {
|
||||
const { ref } = require('vue');
|
||||
|
||||
@@ -4,9 +4,11 @@ import { mount } from '@vue/test-utils';
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: (key) => key
|
||||
}),
|
||||
,
|
||||
locale: require('vue').ref('en')
|
||||
}),
|
||||
createI18n: () => ({
|
||||
global: { t: (key) => key },
|
||||
global: { t: (key) => key , locale: require('vue').ref('en') },
|
||||
install: vi.fn()
|
||||
})
|
||||
}));
|
||||
|
||||
@@ -80,9 +80,11 @@ vi.mock('../../../../service/request', () => ({
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: (key) => key
|
||||
}),
|
||||
,
|
||||
locale: require('vue').ref('en')
|
||||
}),
|
||||
createI18n: () => ({
|
||||
global: { t: (key) => key },
|
||||
global: { t: (key) => key , locale: require('vue').ref('en') },
|
||||
install: vi.fn()
|
||||
})
|
||||
}));
|
||||
|
||||
@@ -5,9 +5,11 @@ vi.mock('vue-sonner', () => ({ toast: { success: vi.fn(), error: vi.fn() } }));
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: (key) => key
|
||||
}),
|
||||
,
|
||||
locale: require('vue').ref('en')
|
||||
}),
|
||||
createI18n: () => ({
|
||||
global: { t: (key) => key },
|
||||
global: { t: (key) => key , locale: require('vue').ref('en') },
|
||||
install: vi.fn()
|
||||
})
|
||||
}));
|
||||
|
||||
@@ -19,538 +19,7 @@
|
||||
:unmount-on-hide="false"
|
||||
@update:modelValue="userDialogTabClick">
|
||||
<template #Info>
|
||||
<template v-if="isFriendOnline(userDialog.friend) || currentUser.id === userDialog.id">
|
||||
<div
|
||||
class="mb-2 pb-2 border-b border-border"
|
||||
v-if="userDialog.ref.location"
|
||||
style="display: flex; flex-direction: column">
|
||||
<div style="flex: none">
|
||||
<template v-if="isRealInstance(userDialog.$location.tag)">
|
||||
<InstanceActionBar
|
||||
class="mb-1"
|
||||
:location="userDialog.$location.tag"
|
||||
:shortname="userDialog.$location.shortName"
|
||||
:currentlocation="lastLocation.location"
|
||||
:instance="userDialog.instance.ref"
|
||||
:friendcount="userDialog.instance.friendCount"
|
||||
:refresh-tooltip="t('dialog.user.info.refresh_instance_info')"
|
||||
:on-refresh="() => refreshInstancePlayerCount(userDialog.$location.tag)" />
|
||||
</template>
|
||||
<Location
|
||||
class="text-sm"
|
||||
:location="userDialog.ref.location"
|
||||
:traveling="userDialog.ref.travelingToLocation" />
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-wrap items-start"
|
||||
style="flex: 1; margin-top: 8px; max-height: 150px; overflow: auto">
|
||||
<div
|
||||
v-if="userDialog.$location.userId"
|
||||
class="box-border flex items-center p-1.5 text-[13px] cursor-pointer w-[167px] hover:rounded-[25px_5px_5px_25px]"
|
||||
@click="showUserDialog(userDialog.$location.userId)">
|
||||
<template v-if="userDialog.$location.user">
|
||||
<div
|
||||
class="relative inline-block flex-none size-9 mr-2.5"
|
||||
:class="userStatusClass(userDialog.$location.user)">
|
||||
<img
|
||||
class="size-full rounded-full object-cover"
|
||||
:src="userImage(userDialog.$location.user, true)"
|
||||
loading="lazy" />
|
||||
</div>
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<span
|
||||
class="block truncate font-medium leading-[18px]"
|
||||
:style="{ color: userDialog.$location.user.$userColour }"
|
||||
v-text="userDialog.$location.user.displayName"></span>
|
||||
<span class="block truncate text-xs">{{
|
||||
t('dialog.user.info.instance_creator')
|
||||
}}</span>
|
||||
</div>
|
||||
</template>
|
||||
<span v-else v-text="userDialog.$location.userId"></span>
|
||||
</div>
|
||||
<div
|
||||
v-for="user in userDialog.users"
|
||||
: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"
|
||||
:class="userStatusClass(user)">
|
||||
<img
|
||||
class="size-full rounded-full object-cover"
|
||||
:src="userImage(user, true)"
|
||||
loading="lazy" />
|
||||
</div>
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<span
|
||||
class="block truncate font-medium leading-[18px]"
|
||||
:style="{ color: user.$userColour }"
|
||||
v-text="user.displayName"></span>
|
||||
<span v-if="user.location === 'traveling'" class="block truncate text-xs">
|
||||
<Spinner class="inline-block mr-1" />
|
||||
<Timer :epoch="user.$travelingToTime" />
|
||||
</span>
|
||||
<span v-else class="block truncate text-xs">
|
||||
<Timer :epoch="user.$location_at" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="flex flex-wrap items-start px-2.5" style="max-height: none">
|
||||
<div
|
||||
v-if="userDialog.note && !hideUserNotes"
|
||||
class="box-border flex items-center p-1.5 text-[13px] w-full cursor-pointer">
|
||||
<div class="flex-1 overflow-hidden" @click="isEditNoteAndMemoDialogVisible = true">
|
||||
<span class="block truncate font-medium leading-[18px]">{{
|
||||
t('dialog.user.info.note')
|
||||
}}</span>
|
||||
<pre
|
||||
class="text-xs"
|
||||
style="
|
||||
font-family: inherit;
|
||||
font-size: 12px;
|
||||
white-space: pre-wrap;
|
||||
margin: 0 0.5em 0 0;
|
||||
max-height: 210px;
|
||||
overflow-y: auto;
|
||||
"
|
||||
>{{ userDialog.note }}</pre
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="userDialog.memo && !hideUserMemos"
|
||||
class="box-border flex items-center p-1.5 text-[13px] w-full cursor-pointer">
|
||||
<div class="flex-1 overflow-hidden" @click="isEditNoteAndMemoDialogVisible = true">
|
||||
<span class="block truncate font-medium leading-[18px]">{{
|
||||
t('dialog.user.info.memo')
|
||||
}}</span>
|
||||
<pre
|
||||
class="text-xs"
|
||||
style="
|
||||
font-family: inherit;
|
||||
font-size: 12px;
|
||||
white-space: pre-wrap;
|
||||
margin: 0 0.5em 0 0;
|
||||
max-height: 210px;
|
||||
overflow-y: auto;
|
||||
"
|
||||
>{{ userDialog.memo }}</pre
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-border flex items-center p-1.5 text-[13px] w-full cursor-default">
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<span class="block truncate font-medium leading-[18px]">
|
||||
{{
|
||||
userDialog.id !== currentUser.id &&
|
||||
userDialog.ref.profilePicOverride &&
|
||||
userDialog.ref.currentAvatarImageUrl
|
||||
? t('dialog.user.info.avatar_info_last_seen')
|
||||
: t('dialog.user.info.avatar_info')
|
||||
}}
|
||||
<TooltipWrapper
|
||||
v-if="userDialog.ref.profilePicOverride && !userDialog.ref.currentAvatarImageUrl"
|
||||
side="top"
|
||||
:content="t('dialog.user.info.vrcplus_hides_avatar')">
|
||||
<Info class="inline-block" />
|
||||
</TooltipWrapper>
|
||||
</span>
|
||||
<div class="text-xs">
|
||||
<AvatarInfo
|
||||
:key="userDialog.id"
|
||||
:imageurl="userDialog.ref.currentAvatarImageUrl"
|
||||
:userid="userDialog.id"
|
||||
:avatartags="userDialog.ref.currentAvatarTags"
|
||||
style="display: inline-block" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-border flex items-center p-1.5 text-[13px] w-full cursor-default">
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<span class="block truncate font-medium leading-[18px]" style="margin-bottom: 6px">{{
|
||||
t('dialog.user.info.represented_group')
|
||||
}}</span>
|
||||
<div
|
||||
v-if="
|
||||
userDialog.isRepresentedGroupLoading ||
|
||||
(userDialog.representedGroup && userDialog.representedGroup.isRepresenting)
|
||||
"
|
||||
class="text-xs">
|
||||
<div style="display: inline-block; flex: none; margin-right: 6px">
|
||||
<Avatar
|
||||
class="cursor-pointer size-15! rounded-lg!"
|
||||
:style="{
|
||||
background: userDialog.isRepresentedGroupLoading ? 'var(--muted)' : ''
|
||||
}"
|
||||
@click="showFullscreenImageDialog(userDialog.representedGroup.iconUrl)">
|
||||
<AvatarImage
|
||||
:src="userDialog.representedGroup.$thumbnailUrl"
|
||||
@load="userDialog.isRepresentedGroupLoading = false"
|
||||
@error="userDialog.isRepresentedGroupLoading = false" />
|
||||
<AvatarFallback class="rounded-lg!" />
|
||||
</Avatar>
|
||||
</div>
|
||||
<span
|
||||
v-if="userDialog.representedGroup.isRepresenting"
|
||||
style="vertical-align: top; cursor: pointer"
|
||||
@click="showGroupDialog(userDialog.representedGroup.groupId)">
|
||||
<span
|
||||
v-if="userDialog.representedGroup.ownerId === userDialog.id"
|
||||
style="margin-right: 6px"
|
||||
>👑</span
|
||||
>
|
||||
<span style="margin-right: 6px" v-text="userDialog.representedGroup.name"></span>
|
||||
<span>({{ userDialog.representedGroup.memberCount }})</span>
|
||||
</span>
|
||||
</div>
|
||||
<div v-else class="text-xs">-</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-border flex items-center p-1.5 text-[13px] w-full cursor-default">
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<span class="block truncate font-medium leading-[18px]">{{
|
||||
t('dialog.user.info.bio')
|
||||
}}</span>
|
||||
<pre
|
||||
class="text-xs truncate"
|
||||
style="
|
||||
font-family: inherit;
|
||||
font-size: 12px;
|
||||
white-space: pre-wrap;
|
||||
margin: 0 0.5em 0 0;
|
||||
max-height: 210px;
|
||||
overflow-y: auto;
|
||||
"
|
||||
>{{ bioCache.translated || userDialog.ref.bio || '-' }}</pre
|
||||
>
|
||||
<div style="float: right">
|
||||
<Button
|
||||
v-if="translationApi && userDialog.ref.bio"
|
||||
class="w-3 h-6 text-xs mr-0.5"
|
||||
size="icon-sm"
|
||||
variant="ghost"
|
||||
@click="translateBio">
|
||||
<Spinner v-if="translateLoading" class="size-1" />
|
||||
<Languages v-else class="h-3 w-3" />
|
||||
</Button>
|
||||
<Button
|
||||
class="w-3 h-6 text-xs"
|
||||
size="icon-sm"
|
||||
variant="ghost"
|
||||
v-if="userDialog.id === currentUser.id"
|
||||
style="margin-left: 6px; padding: 0"
|
||||
@click="showBioDialog"
|
||||
><Pencil class="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
<div style="margin-top: 6px" class="flex items-center">
|
||||
<TooltipWrapper v-for="(link, index) in userDialog.ref.bioLinks" :key="index">
|
||||
<template #content>
|
||||
<span v-text="link"></span>
|
||||
</template>
|
||||
<!-- onerror="this.onerror=null;this.class='icon-error'" -->
|
||||
<img
|
||||
:src="getFaviconUrl(link)"
|
||||
style="
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
vertical-align: middle;
|
||||
margin-right: 6px;
|
||||
cursor: pointer;
|
||||
"
|
||||
@click.stop="openExternalLink(link)"
|
||||
loading="lazy" />
|
||||
</TooltipWrapper>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="currentUser.id !== userDialog.id">
|
||||
<div class="box-border flex items-center p-1.5 text-[13px] cursor-default w-[167px]">
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<span class="block truncate font-medium leading-[18px]">
|
||||
{{ t('dialog.user.info.last_seen') }}
|
||||
</span>
|
||||
<span class="block truncate text-xs">{{
|
||||
formatDateFilter(userDialog.lastSeen, 'long')
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="box-border flex items-center p-1.5 text-[13px] cursor-pointer w-[167px]"
|
||||
@click="showPreviousInstancesListDialog(userDialog.ref)">
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<div
|
||||
class="block truncate font-medium leading-[18px]"
|
||||
style="display: flex; justify-content: space-between; align-items: center">
|
||||
<div>
|
||||
{{ t('dialog.user.info.join_count') }}
|
||||
</div>
|
||||
|
||||
<TooltipWrapper side="top" :content="t('dialog.user.info.open_previous_instance')">
|
||||
<MoreHorizontal style="margin-right: 16px" />
|
||||
</TooltipWrapper>
|
||||
</div>
|
||||
<span v-if="userDialog.joinCount === 0" class="block truncate text-xs">-</span>
|
||||
<span v-else class="block truncate text-xs" v-text="userDialog.joinCount"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="box-border flex items-center p-1.5 text-[13px] cursor-default w-[167px]">
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<span class="block truncate font-medium leading-[18px]">
|
||||
{{ t('dialog.user.info.time_together') }}
|
||||
</span>
|
||||
<span v-if="userDialog.timeSpent === 0" class="block truncate text-xs">-</span>
|
||||
<span v-else class="block truncate text-xs">{{
|
||||
timeToText(userDialog.timeSpent)
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<TooltipWrapper
|
||||
:disabled="currentUser.id !== userDialog.id"
|
||||
side="top"
|
||||
:content="t('dialog.user.info.open_previous_instance')">
|
||||
<div
|
||||
class="box-border flex items-center p-1.5 text-[13px] cursor-pointer w-[167px]"
|
||||
@click="showPreviousInstancesListDialog(userDialog.ref)">
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<span class="block truncate font-medium leading-[18px]">
|
||||
{{ t('dialog.user.info.play_time') }}
|
||||
</span>
|
||||
<span v-if="userDialog.timeSpent === 0" class="block truncate text-xs">-</span>
|
||||
<span v-else class="block truncate text-xs">{{
|
||||
timeToText(userDialog.timeSpent)
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</TooltipWrapper>
|
||||
</template>
|
||||
<div class="box-border flex items-center p-1.5 text-[13px] cursor-default w-[167px]">
|
||||
<TooltipWrapper :side="currentUser.id !== userDialog.id ? 'bottom' : 'top'">
|
||||
<template #content>
|
||||
<span>{{ formatDateFilter(userOnlineForTimestamp(userDialog), 'short') }}</span>
|
||||
</template>
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<span
|
||||
v-if="userDialog.ref.state === 'online' && userDialog.ref.$online_for"
|
||||
class="block truncate font-medium leading-[18px]">
|
||||
{{ t('dialog.user.info.online_for') }}
|
||||
</span>
|
||||
<span v-else class="block truncate font-medium leading-[18px]">
|
||||
{{ t('dialog.user.info.offline_for') }}
|
||||
</span>
|
||||
<span class="block truncate text-xs">{{ userOnlineFor(userDialog.ref) }}</span>
|
||||
</div>
|
||||
</TooltipWrapper>
|
||||
</div>
|
||||
<div class="box-border flex items-center p-1.5 text-[13px] cursor-default w-[167px]">
|
||||
<TooltipWrapper :side="currentUser.id !== userDialog.id ? 'bottom' : 'top'">
|
||||
<template #content>
|
||||
<span
|
||||
>{{ t('dialog.user.info.last_login') }}
|
||||
{{ formatDateFilter(userDialog.ref.last_login, 'long') }}</span
|
||||
>
|
||||
<br />
|
||||
<span
|
||||
>{{ t('dialog.user.info.last_activity') }}
|
||||
{{ formatDateFilter(userDialog.ref.last_activity, 'long') }}</span
|
||||
>
|
||||
</template>
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<span class="block truncate font-medium leading-[18px]">{{
|
||||
t('dialog.user.info.last_activity')
|
||||
}}</span>
|
||||
<span v-if="userDialog.ref.last_activity" class="block truncate text-xs">{{
|
||||
timeToText(Date.now() - Date.parse(userDialog.ref.last_activity))
|
||||
}}</span>
|
||||
<span v-else class="block truncate text-xs">-</span>
|
||||
</div>
|
||||
</TooltipWrapper>
|
||||
</div>
|
||||
<div class="box-border flex items-center p-1.5 text-[13px] cursor-default w-[167px]">
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<span class="block truncate font-medium leading-[18px]">{{
|
||||
t('dialog.user.info.date_joined')
|
||||
}}</span>
|
||||
<span class="block truncate text-xs" v-text="userDialog.ref.date_joined"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="currentUser.id !== userDialog.id"
|
||||
class="box-border flex items-center p-1.5 text-[13px] cursor-default w-[167px]">
|
||||
<TooltipWrapper side="top" :disabled="userDialog.dateFriendedInfo.length < 2">
|
||||
<template #content>
|
||||
<template v-for="ref in userDialog.dateFriendedInfo" :key="ref.type">
|
||||
<span>{{ ref.type }}: {{ formatDateFilter(ref.created_at, 'long') }}</span
|
||||
><br />
|
||||
</template>
|
||||
</template>
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<span v-if="userDialog.unFriended" class="block truncate font-medium leading-[18px]">
|
||||
{{ t('dialog.user.info.unfriended') }}
|
||||
</span>
|
||||
<span v-else class="block truncate font-medium leading-[18px]">
|
||||
{{ t('dialog.user.info.friended') }}
|
||||
</span>
|
||||
<span class="block truncate text-xs">{{
|
||||
formatDateFilter(userDialog.dateFriended, 'long')
|
||||
}}</span>
|
||||
</div>
|
||||
</TooltipWrapper>
|
||||
</div>
|
||||
<template v-if="currentUser.id === userDialog.id">
|
||||
<div
|
||||
class="box-border flex items-center p-1.5 text-[13px] cursor-pointer w-[167px]"
|
||||
@click="toggleAvatarCopying">
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<span class="block truncate font-medium leading-[18px]">{{
|
||||
t('dialog.user.info.avatar_cloning')
|
||||
}}</span>
|
||||
<span v-if="currentUser.allowAvatarCopying" class="block truncate text-xs">{{
|
||||
t('dialog.user.info.avatar_cloning_allow')
|
||||
}}</span>
|
||||
<span v-else class="block truncate text-xs">{{
|
||||
t('dialog.user.info.avatar_cloning_deny')
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="box-border flex items-center p-1.5 text-[13px] cursor-pointer w-[167px]"
|
||||
@click="toggleAllowBooping">
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<span class="block truncate font-medium leading-[18px]">{{
|
||||
t('dialog.user.info.booping')
|
||||
}}</span>
|
||||
<span v-if="currentUser.isBoopingEnabled" class="block truncate text-xs">{{
|
||||
t('dialog.user.info.avatar_cloning_allow')
|
||||
}}</span>
|
||||
<span v-else class="block truncate text-xs">{{
|
||||
t('dialog.user.info.avatar_cloning_deny')
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="box-border flex items-center p-1.5 text-[13px] cursor-pointer w-[167px]"
|
||||
@click="toggleSharedConnectionsOptOut">
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<span class="block truncate font-medium leading-[18px]">{{
|
||||
t('dialog.user.info.show_mutual_friends')
|
||||
}}</span>
|
||||
<span v-if="!currentUser.hasSharedConnectionsOptOut" class="block truncate text-xs">{{
|
||||
t('dialog.user.info.avatar_cloning_allow')
|
||||
}}</span>
|
||||
<span v-else class="block truncate text-xs">{{
|
||||
t('dialog.user.info.avatar_cloning_deny')
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="box-border flex items-center p-1.5 text-[13px] cursor-pointer w-[167px]"
|
||||
@click="toggleDiscordFriendsOptOut">
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<span class="block truncate font-medium leading-[18px]">{{
|
||||
t('dialog.user.info.show_discord_connections')
|
||||
}}</span>
|
||||
<span v-if="!currentUser.hasDiscordFriendsOptOut" class="block truncate text-xs">{{
|
||||
t('dialog.user.info.avatar_cloning_allow')
|
||||
}}</span>
|
||||
<span v-else class="block truncate text-xs">{{
|
||||
t('dialog.user.info.avatar_cloning_deny')
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="box-border flex items-center p-1.5 text-[13px] cursor-default w-[167px]">
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<span class="block truncate font-medium leading-[18px]">{{
|
||||
t('dialog.user.info.avatar_cloning')
|
||||
}}</span>
|
||||
<span v-if="userDialog.ref.allowAvatarCopying" class="block truncate text-xs">{{
|
||||
t('dialog.user.info.avatar_cloning_allow')
|
||||
}}</span>
|
||||
<span v-else class="block truncate text-xs">{{
|
||||
t('dialog.user.info.avatar_cloning_deny')
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div
|
||||
v-if="userDialog.ref.id === currentUser.id"
|
||||
class="box-border flex items-center p-1.5 text-[13px] cursor-pointer w-[167px]"
|
||||
@click="getVRChatCredits()">
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<span class="block truncate font-medium leading-[18px]">{{
|
||||
t('view.profile.profile.vrchat_credits')
|
||||
}}</span>
|
||||
<span class="block truncate text-xs">{{
|
||||
vrchatCredit ?? t('view.profile.profile.refresh')
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="userDialog.ref.id === currentUser.id && currentUser.homeLocation"
|
||||
class="box-border flex items-center p-1.5 text-[13px] w-full cursor-pointer"
|
||||
@click="showWorldDialog(currentUser.homeLocation)">
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<span class="block truncate font-medium leading-[18px]">{{
|
||||
t('dialog.user.info.home_location')
|
||||
}}</span>
|
||||
<span class="block truncate text-xs">
|
||||
<span v-text="userDialog.$homeLocationName"></span>
|
||||
<Button
|
||||
class="rounded-full ml-1 text-xs"
|
||||
size="icon-sm"
|
||||
variant="ghost"
|
||||
@click.stop="resetHome()"
|
||||
><Trash2 class="h-4 w-4" />
|
||||
</Button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-border flex items-center p-1.5 text-[13px] w-full cursor-default">
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<span class="block truncate font-medium leading-[18px]">{{
|
||||
t('dialog.user.info.id')
|
||||
}}</span>
|
||||
<span class="block truncate text-xs">
|
||||
{{ userDialog.id }}
|
||||
<TooltipWrapper side="top" :content="t('dialog.user.info.id_tooltip')">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger as-child>
|
||||
<Button
|
||||
class="rounded-full ml-1 text-xs"
|
||||
size="icon-sm"
|
||||
variant="ghost"
|
||||
@click.stop
|
||||
><Copy class="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem @click="copyUserId(userDialog.id)">
|
||||
{{ t('dialog.user.info.copy_id') }}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem @click="copyUserURL(userDialog.id)">
|
||||
{{ t('dialog.user.info.copy_url') }}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem @click="copyUserDisplayName(userDialog.ref.displayName)">
|
||||
{{ t('dialog.user.info.copy_display_name') }}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TooltipWrapper>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<UserDialogInfoTab ref="infoTabRef" @show-bio-dialog="showBioDialog" />
|
||||
</template>
|
||||
|
||||
<template v-if="userDialog.id !== currentUser.id && !currentUser.hasSharedConnectionsOptOut" #mutual>
|
||||
@@ -597,31 +66,18 @@
|
||||
<BioDialog :bio-dialog="bioDialog" />
|
||||
<PronounsDialog :pronouns-dialog="pronounsDialog" />
|
||||
<ModerateGroupDialog />
|
||||
<EditNoteAndMemoDialog v-model:visible="isEditNoteAndMemoDialogVisible" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Copy, Info, Languages, MoreHorizontal, Pencil, Trash2 } from 'lucide-vue-next';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { computed, defineAsyncComponent, ref, watch } from 'vue';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
import { DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Spinner } from '@/components/ui/spinner';
|
||||
import { TabsUnderline } from '@/components/ui/tabs';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { toast } from 'vue-sonner';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import {
|
||||
useAdvancedSettingsStore,
|
||||
useAppearanceSettingsStore,
|
||||
useAvatarStore,
|
||||
useFavoriteStore,
|
||||
useFriendStore,
|
||||
@@ -633,33 +89,19 @@
|
||||
useModalStore,
|
||||
useModerationStore,
|
||||
useNotificationStore,
|
||||
useUserStore,
|
||||
useWorldStore
|
||||
useUserStore
|
||||
} from '../../../stores';
|
||||
import {
|
||||
copyToClipboard,
|
||||
formatDateFilter,
|
||||
getFaviconUrl,
|
||||
isFriendOnline,
|
||||
isRealInstance,
|
||||
openExternalLink,
|
||||
refreshInstancePlayerCount,
|
||||
timeToText,
|
||||
userImage,
|
||||
userOnlineFor,
|
||||
userOnlineForTimestamp,
|
||||
userStatusClass
|
||||
} from '../../../shared/utils';
|
||||
import { miscRequest, userRequest } from '../../../api';
|
||||
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 InstanceActionBar from '../../InstanceActionBar.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';
|
||||
@@ -670,7 +112,6 @@
|
||||
const SendInviteRequestDialog = defineAsyncComponent(() => import('./SendInviteRequestDialog.vue'));
|
||||
const SocialStatusDialog = defineAsyncComponent(() => import('./SocialStatusDialog.vue'));
|
||||
const ModerateGroupDialog = defineAsyncComponent(() => import('../ModerateGroupDialog.vue'));
|
||||
const EditNoteAndMemoDialog = defineAsyncComponent(() => import('./EditNoteAndMemoDialog.vue'));
|
||||
|
||||
const { t } = useI18n();
|
||||
const userDialogTabs = computed(() => {
|
||||
@@ -687,6 +128,7 @@
|
||||
}
|
||||
return tabs;
|
||||
});
|
||||
const infoTabRef = ref(null);
|
||||
const favoriteWorldsTabRef = ref(null);
|
||||
const mutualFriendsTabRef = ref(null);
|
||||
const worldsTabRef = ref(null);
|
||||
@@ -696,28 +138,18 @@
|
||||
const modalStore = useModalStore();
|
||||
const instanceStore = useInstanceStore();
|
||||
|
||||
const { hideUserNotes, hideUserMemos, isDarkMode } = storeToRefs(useAppearanceSettingsStore());
|
||||
const { bioLanguage, translationApi, translationApiType } = storeToRefs(useAdvancedSettingsStore());
|
||||
const { translateText } = useAdvancedSettingsStore();
|
||||
const { userDialog, languageDialog, currentUser, isLocalUserVrcPlusSupporter } = storeToRefs(useUserStore());
|
||||
const {
|
||||
cachedUsers,
|
||||
showUserDialog,
|
||||
refreshUserDialogAvatars,
|
||||
showSendBoopDialog,
|
||||
toggleSharedConnectionsOptOut,
|
||||
toggleDiscordFriendsOptOut
|
||||
} = useUserStore();
|
||||
const { cachedUsers, showUserDialog, refreshUserDialogAvatars, showSendBoopDialog } = useUserStore();
|
||||
const { showFavoriteDialog } = useFavoriteStore();
|
||||
const { showAvatarDialog, showAvatarAuthorDialog } = useAvatarStore();
|
||||
const { showWorldDialog } = useWorldStore();
|
||||
|
||||
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, showFullscreenImageDialog, showGalleryPage } = useGalleryStore();
|
||||
const { clearInviteImageUpload, showGalleryPage } = useGalleryStore();
|
||||
|
||||
const { applyPlayerModeration, handlePlayerModerationDelete } = useModerationStore();
|
||||
|
||||
@@ -759,12 +191,6 @@
|
||||
() => {
|
||||
if (userDialog.value.visible) {
|
||||
!userDialog.value.loading && loadLastActiveTab();
|
||||
if (userDialog.value.id !== bioCache.value.userId) {
|
||||
bioCache.value = {
|
||||
userId: null,
|
||||
translated: null
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -794,21 +220,11 @@
|
||||
bioLinks: []
|
||||
});
|
||||
|
||||
const translateLoading = ref(false);
|
||||
|
||||
const pronounsDialog = ref({
|
||||
visible: false,
|
||||
loading: false,
|
||||
pronouns: ''
|
||||
});
|
||||
|
||||
const bioCache = ref({
|
||||
userId: null,
|
||||
translated: null
|
||||
});
|
||||
|
||||
const isEditNoteAndMemoDialogVisible = ref(false);
|
||||
const vrchatCredit = ref(null);
|
||||
const treeData = ref({});
|
||||
|
||||
/**
|
||||
@@ -873,9 +289,7 @@
|
||||
userDialog.value.lastActiveTab = tabName;
|
||||
const userId = userDialog.value.id;
|
||||
if (tabName === 'Info') {
|
||||
if (vrchatCredit.value === null) {
|
||||
getVRChatCredits();
|
||||
}
|
||||
infoTabRef.value?.onTabActivated();
|
||||
} else if (tabName === 'mutual') {
|
||||
if (userId === currentUser.value.id) {
|
||||
userDialog.value.activeTab = 'Info';
|
||||
@@ -962,7 +376,7 @@
|
||||
showBioDialog,
|
||||
showPronounsDialog,
|
||||
showEditNoteAndMemoDialog: () => {
|
||||
isEditNoteAndMemoDialogVisible.value = true;
|
||||
infoTabRef.value?.showEditNoteAndMemoDialog();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1038,113 +452,6 @@
|
||||
D.visible = true;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
async function translateBio() {
|
||||
if (translateLoading.value) {
|
||||
return;
|
||||
}
|
||||
const bio = userDialog.value.ref.bio;
|
||||
if (!bio) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetLang = bioLanguage.value;
|
||||
|
||||
if (bioCache.value.userId !== userDialog.value.id) {
|
||||
bioCache.value.userId = userDialog.value.id;
|
||||
bioCache.value.translated = null;
|
||||
}
|
||||
|
||||
if (bioCache.value.translated) {
|
||||
bioCache.value.translated = null;
|
||||
return;
|
||||
}
|
||||
|
||||
translateLoading.value = true;
|
||||
try {
|
||||
const providerLabel = translationApiType.value === 'openai' ? 'OpenAI' : 'Google';
|
||||
const translated = await translateText(`${bio}\n\nTranslated by ${providerLabel}`, targetLang);
|
||||
if (!translated) {
|
||||
throw new Error('No translation returned');
|
||||
}
|
||||
|
||||
bioCache.value.translated = translated;
|
||||
} catch (err) {
|
||||
console.error('Translation failed:', err);
|
||||
} finally {
|
||||
translateLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param userRef
|
||||
*/
|
||||
function showPreviousInstancesListDialog(userRef) {
|
||||
instanceStore.showPreviousInstancesListDialog('user', userRef);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function toggleAvatarCopying() {
|
||||
userRequest.saveCurrentUser({
|
||||
allowAvatarCopying: !currentUser.value.allowAvatarCopying
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function toggleAllowBooping() {
|
||||
userRequest.saveCurrentUser({
|
||||
isBoopingEnabled: !currentUser.value.isBoopingEnabled
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function resetHome() {
|
||||
modalStore
|
||||
.confirm({
|
||||
description: t('confirm.command_question', {
|
||||
command: t('dialog.user.actions.reset_home')
|
||||
}),
|
||||
title: t('confirm.title')
|
||||
})
|
||||
.then(({ ok }) => {
|
||||
if (!ok) return;
|
||||
userRequest
|
||||
.saveCurrentUser({
|
||||
homeLocation: ''
|
||||
})
|
||||
.then((args) => {
|
||||
toast.success('Home world has been reset');
|
||||
return args;
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param userId
|
||||
*/
|
||||
function copyUserId(userId) {
|
||||
copyToClipboard(userId, 'User ID copied to clipboard');
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param userId
|
||||
*/
|
||||
function copyUserURL(userId) {
|
||||
copyToClipboard(`https://vrchat.com/home/user/${userId}`, 'User URL copied to clipboard');
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param displayName
|
||||
@@ -1159,11 +466,4 @@
|
||||
function closeInviteDialog() {
|
||||
clearInviteImageUpload();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function getVRChatCredits() {
|
||||
miscRequest.getVRChatCredits().then((args) => (vrchatCredit.value = args.json?.balance));
|
||||
}
|
||||
</script>
|
||||
|
||||
712
src/components/dialogs/UserDialog/UserDialogInfoTab.vue
Normal file
712
src/components/dialogs/UserDialog/UserDialogInfoTab.vue
Normal file
@@ -0,0 +1,712 @@
|
||||
<template>
|
||||
<template v-if="isFriendOnline(userDialog.friend) || currentUser.id === userDialog.id">
|
||||
<div
|
||||
class="mb-2 pb-2 border-b border-border"
|
||||
v-if="userDialog.ref.location"
|
||||
style="display: flex; flex-direction: column">
|
||||
<div style="flex: none">
|
||||
<template v-if="isRealInstance(userDialog.$location.tag)">
|
||||
<InstanceActionBar
|
||||
class="mb-1"
|
||||
:location="userDialog.$location.tag"
|
||||
:shortname="userDialog.$location.shortName"
|
||||
:currentlocation="lastLocation.location"
|
||||
:instance="userDialog.instance.ref"
|
||||
:friendcount="userDialog.instance.friendCount"
|
||||
:refresh-tooltip="t('dialog.user.info.refresh_instance_info')"
|
||||
:on-refresh="() => refreshInstancePlayerCount(userDialog.$location.tag)" />
|
||||
</template>
|
||||
<Location
|
||||
class="text-sm"
|
||||
:location="userDialog.ref.location"
|
||||
:traveling="userDialog.ref.travelingToLocation" />
|
||||
</div>
|
||||
<div class="flex flex-wrap items-start" style="flex: 1; margin-top: 8px; max-height: 150px; overflow: auto">
|
||||
<div
|
||||
v-if="userDialog.$location.userId"
|
||||
class="box-border flex items-center p-1.5 text-[13px] cursor-pointer w-[167px] hover:rounded-[25px_5px_5px_25px]"
|
||||
@click="showUserDialog(userDialog.$location.userId)">
|
||||
<template v-if="userDialog.$location.user">
|
||||
<div
|
||||
class="relative inline-block flex-none size-9 mr-2.5"
|
||||
:class="userStatusClass(userDialog.$location.user)">
|
||||
<img
|
||||
class="size-full rounded-full object-cover"
|
||||
:src="userImage(userDialog.$location.user, true)"
|
||||
loading="lazy" />
|
||||
</div>
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<span
|
||||
class="block truncate font-medium leading-[18px]"
|
||||
:style="{ color: userDialog.$location.user.$userColour }"
|
||||
v-text="userDialog.$location.user.displayName"></span>
|
||||
<span class="block truncate text-xs">{{ t('dialog.user.info.instance_creator') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<span v-else v-text="userDialog.$location.userId"></span>
|
||||
</div>
|
||||
<div
|
||||
v-for="user in userDialog.users"
|
||||
: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" :class="userStatusClass(user)">
|
||||
<img class="size-full rounded-full object-cover" :src="userImage(user, true)" loading="lazy" />
|
||||
</div>
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<span
|
||||
class="block truncate font-medium leading-[18px]"
|
||||
:style="{ color: user.$userColour }"
|
||||
v-text="user.displayName"></span>
|
||||
<span v-if="user.location === 'traveling'" class="block truncate text-xs">
|
||||
<Spinner class="inline-block mr-1" />
|
||||
<Timer :epoch="user.$travelingToTime" />
|
||||
</span>
|
||||
<span v-else class="block truncate text-xs">
|
||||
<Timer :epoch="user.$location_at" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="flex flex-wrap items-start px-2.5" style="max-height: none">
|
||||
<div
|
||||
v-if="userDialog.note && !hideUserNotes"
|
||||
class="box-border flex items-center p-1.5 text-[13px] w-full cursor-pointer">
|
||||
<div class="flex-1 overflow-hidden" @click="isEditNoteAndMemoDialogVisible = true">
|
||||
<span class="block truncate font-medium leading-[18px]">{{ t('dialog.user.info.note') }}</span>
|
||||
<pre
|
||||
class="text-xs"
|
||||
style="
|
||||
font-family: inherit;
|
||||
font-size: 12px;
|
||||
white-space: pre-wrap;
|
||||
margin: 0 0.5em 0 0;
|
||||
max-height: 210px;
|
||||
overflow-y: auto;
|
||||
"
|
||||
>{{ userDialog.note }}</pre
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="userDialog.memo && !hideUserMemos"
|
||||
class="box-border flex items-center p-1.5 text-[13px] w-full cursor-pointer">
|
||||
<div class="flex-1 overflow-hidden" @click="isEditNoteAndMemoDialogVisible = true">
|
||||
<span class="block truncate font-medium leading-[18px]">{{ t('dialog.user.info.memo') }}</span>
|
||||
<pre
|
||||
class="text-xs"
|
||||
style="
|
||||
font-family: inherit;
|
||||
font-size: 12px;
|
||||
white-space: pre-wrap;
|
||||
margin: 0 0.5em 0 0;
|
||||
max-height: 210px;
|
||||
overflow-y: auto;
|
||||
"
|
||||
>{{ userDialog.memo }}</pre
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-border flex items-center p-1.5 text-[13px] w-full cursor-default">
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<span class="block truncate font-medium leading-[18px]">
|
||||
{{
|
||||
userDialog.id !== currentUser.id &&
|
||||
userDialog.ref.profilePicOverride &&
|
||||
userDialog.ref.currentAvatarImageUrl
|
||||
? t('dialog.user.info.avatar_info_last_seen')
|
||||
: t('dialog.user.info.avatar_info')
|
||||
}}
|
||||
<TooltipWrapper
|
||||
v-if="userDialog.ref.profilePicOverride && !userDialog.ref.currentAvatarImageUrl"
|
||||
side="top"
|
||||
:content="t('dialog.user.info.vrcplus_hides_avatar')">
|
||||
<Info class="inline-block" />
|
||||
</TooltipWrapper>
|
||||
</span>
|
||||
<div class="text-xs">
|
||||
<AvatarInfo
|
||||
:key="userDialog.id"
|
||||
:imageurl="userDialog.ref.currentAvatarImageUrl"
|
||||
:userid="userDialog.id"
|
||||
:avatartags="userDialog.ref.currentAvatarTags"
|
||||
style="display: inline-block" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-border flex items-center p-1.5 text-[13px] w-full cursor-default">
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<span class="block truncate font-medium leading-[18px]" style="margin-bottom: 6px">{{
|
||||
t('dialog.user.info.represented_group')
|
||||
}}</span>
|
||||
<div
|
||||
v-if="
|
||||
userDialog.isRepresentedGroupLoading ||
|
||||
(userDialog.representedGroup && userDialog.representedGroup.isRepresenting)
|
||||
"
|
||||
class="text-xs">
|
||||
<div style="display: inline-block; flex: none; margin-right: 6px">
|
||||
<Avatar
|
||||
class="cursor-pointer size-15! rounded-lg!"
|
||||
:style="{
|
||||
background: userDialog.isRepresentedGroupLoading ? 'var(--muted)' : ''
|
||||
}"
|
||||
@click="showFullscreenImageDialog(userDialog.representedGroup.iconUrl)">
|
||||
<AvatarImage
|
||||
:src="userDialog.representedGroup.$thumbnailUrl"
|
||||
@load="userDialog.isRepresentedGroupLoading = false"
|
||||
@error="userDialog.isRepresentedGroupLoading = false" />
|
||||
<AvatarFallback class="rounded-lg!" />
|
||||
</Avatar>
|
||||
</div>
|
||||
<span
|
||||
v-if="userDialog.representedGroup.isRepresenting"
|
||||
style="vertical-align: top; cursor: pointer"
|
||||
@click="showGroupDialog(userDialog.representedGroup.groupId)">
|
||||
<span v-if="userDialog.representedGroup.ownerId === userDialog.id" style="margin-right: 6px"
|
||||
>👑</span
|
||||
>
|
||||
<span style="margin-right: 6px" v-text="userDialog.representedGroup.name"></span>
|
||||
<span>({{ userDialog.representedGroup.memberCount }})</span>
|
||||
</span>
|
||||
</div>
|
||||
<div v-else class="text-xs">-</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-border flex items-center p-1.5 text-[13px] w-full cursor-default">
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<span class="block truncate font-medium leading-[18px]">{{ t('dialog.user.info.bio') }}</span>
|
||||
<pre
|
||||
class="text-xs truncate"
|
||||
style="
|
||||
font-family: inherit;
|
||||
font-size: 12px;
|
||||
white-space: pre-wrap;
|
||||
margin: 0 0.5em 0 0;
|
||||
max-height: 210px;
|
||||
overflow-y: auto;
|
||||
"
|
||||
>{{ bioCache.translated || userDialog.ref.bio || '-' }}</pre
|
||||
>
|
||||
<div style="float: right">
|
||||
<Button
|
||||
v-if="translationApi && userDialog.ref.bio"
|
||||
class="w-3 h-6 text-xs mr-0.5"
|
||||
size="icon-sm"
|
||||
variant="ghost"
|
||||
@click="translateBio">
|
||||
<Spinner v-if="translateLoading" class="size-1" />
|
||||
<Languages v-else class="h-3 w-3" />
|
||||
</Button>
|
||||
<Button
|
||||
class="w-3 h-6 text-xs"
|
||||
size="icon-sm"
|
||||
variant="ghost"
|
||||
v-if="userDialog.id === currentUser.id"
|
||||
style="margin-left: 6px; padding: 0"
|
||||
@click="$emit('showBioDialog')"
|
||||
><Pencil class="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
<div style="margin-top: 6px" class="flex items-center">
|
||||
<TooltipWrapper v-for="(link, index) in userDialog.ref.bioLinks" :key="index">
|
||||
<template #content>
|
||||
<span v-text="link"></span>
|
||||
</template>
|
||||
<!-- onerror="this.onerror=null;this.class='icon-error'" -->
|
||||
<img
|
||||
:src="getFaviconUrl(link)"
|
||||
style="
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
vertical-align: middle;
|
||||
margin-right: 6px;
|
||||
cursor: pointer;
|
||||
"
|
||||
@click.stop="openExternalLink(link)"
|
||||
loading="lazy" />
|
||||
</TooltipWrapper>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="currentUser.id !== userDialog.id">
|
||||
<div class="box-border flex items-center p-1.5 text-[13px] cursor-default w-[167px]">
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<span class="block truncate font-medium leading-[18px]">
|
||||
{{ t('dialog.user.info.last_seen') }}
|
||||
</span>
|
||||
<span class="block truncate text-xs">{{ formatDateFilter(userDialog.lastSeen, 'long') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="box-border flex items-center p-1.5 text-[13px] cursor-pointer w-[167px]"
|
||||
@click="showPreviousInstancesListDialog(userDialog.ref)">
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<div
|
||||
class="block truncate font-medium leading-[18px]"
|
||||
style="display: flex; justify-content: space-between; align-items: center">
|
||||
<div>
|
||||
{{ t('dialog.user.info.join_count') }}
|
||||
</div>
|
||||
|
||||
<TooltipWrapper side="top" :content="t('dialog.user.info.open_previous_instance')">
|
||||
<MoreHorizontal style="margin-right: 16px" />
|
||||
</TooltipWrapper>
|
||||
</div>
|
||||
<span v-if="userDialog.joinCount === 0" class="block truncate text-xs">-</span>
|
||||
<span v-else class="block truncate text-xs" v-text="userDialog.joinCount"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="box-border flex items-center p-1.5 text-[13px] cursor-default w-[167px]">
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<span class="block truncate font-medium leading-[18px]">
|
||||
{{ t('dialog.user.info.time_together') }}
|
||||
</span>
|
||||
<span v-if="userDialog.timeSpent === 0" class="block truncate text-xs">-</span>
|
||||
<span v-else class="block truncate text-xs">{{ timeToText(userDialog.timeSpent) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<TooltipWrapper
|
||||
:disabled="currentUser.id !== userDialog.id"
|
||||
side="top"
|
||||
:content="t('dialog.user.info.open_previous_instance')">
|
||||
<div
|
||||
class="box-border flex items-center p-1.5 text-[13px] cursor-pointer w-[167px]"
|
||||
@click="showPreviousInstancesListDialog(userDialog.ref)">
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<span class="block truncate font-medium leading-[18px]">
|
||||
{{ t('dialog.user.info.play_time') }}
|
||||
</span>
|
||||
<span v-if="userDialog.timeSpent === 0" class="block truncate text-xs">-</span>
|
||||
<span v-else class="block truncate text-xs">{{ timeToText(userDialog.timeSpent) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</TooltipWrapper>
|
||||
</template>
|
||||
<div class="box-border flex items-center p-1.5 text-[13px] cursor-default w-[167px]">
|
||||
<TooltipWrapper :side="currentUser.id !== userDialog.id ? 'bottom' : 'top'">
|
||||
<template #content>
|
||||
<span>{{ formatDateFilter(userOnlineForTimestamp(userDialog), 'short') }}</span>
|
||||
</template>
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<span
|
||||
v-if="userDialog.ref.state === 'online' && userDialog.ref.$online_for"
|
||||
class="block truncate font-medium leading-[18px]">
|
||||
{{ t('dialog.user.info.online_for') }}
|
||||
</span>
|
||||
<span v-else class="block truncate font-medium leading-[18px]">
|
||||
{{ t('dialog.user.info.offline_for') }}
|
||||
</span>
|
||||
<span class="block truncate text-xs">{{ userOnlineFor(userDialog.ref) }}</span>
|
||||
</div>
|
||||
</TooltipWrapper>
|
||||
</div>
|
||||
<div class="box-border flex items-center p-1.5 text-[13px] cursor-default w-[167px]">
|
||||
<TooltipWrapper :side="currentUser.id !== userDialog.id ? 'bottom' : 'top'">
|
||||
<template #content>
|
||||
<span
|
||||
>{{ t('dialog.user.info.last_login') }}
|
||||
{{ formatDateFilter(userDialog.ref.last_login, 'long') }}</span
|
||||
>
|
||||
<br />
|
||||
<span
|
||||
>{{ t('dialog.user.info.last_activity') }}
|
||||
{{ formatDateFilter(userDialog.ref.last_activity, 'long') }}</span
|
||||
>
|
||||
</template>
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<span class="block truncate font-medium leading-[18px]">{{
|
||||
t('dialog.user.info.last_activity')
|
||||
}}</span>
|
||||
<span v-if="userDialog.ref.last_activity" class="block truncate text-xs">{{
|
||||
timeToText(Date.now() - Date.parse(userDialog.ref.last_activity))
|
||||
}}</span>
|
||||
<span v-else class="block truncate text-xs">-</span>
|
||||
</div>
|
||||
</TooltipWrapper>
|
||||
</div>
|
||||
<div class="box-border flex items-center p-1.5 text-[13px] cursor-default w-[167px]">
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<span class="block truncate font-medium leading-[18px]">{{ t('dialog.user.info.date_joined') }}</span>
|
||||
<span class="block truncate text-xs" v-text="userDialog.ref.date_joined"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="currentUser.id !== userDialog.id"
|
||||
class="box-border flex items-center p-1.5 text-[13px] cursor-default w-[167px]">
|
||||
<TooltipWrapper side="top" :disabled="userDialog.dateFriendedInfo.length < 2">
|
||||
<template #content>
|
||||
<template v-for="ref in userDialog.dateFriendedInfo" :key="ref.type">
|
||||
<span>{{ ref.type }}: {{ formatDateFilter(ref.created_at, 'long') }}</span
|
||||
><br />
|
||||
</template>
|
||||
</template>
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<span v-if="userDialog.unFriended" class="block truncate font-medium leading-[18px]">
|
||||
{{ t('dialog.user.info.unfriended') }}
|
||||
</span>
|
||||
<span v-else class="block truncate font-medium leading-[18px]">
|
||||
{{ t('dialog.user.info.friended') }}
|
||||
</span>
|
||||
<span class="block truncate text-xs">{{ formatDateFilter(userDialog.dateFriended, 'long') }}</span>
|
||||
</div>
|
||||
</TooltipWrapper>
|
||||
</div>
|
||||
<template v-if="currentUser.id === userDialog.id">
|
||||
<div
|
||||
class="box-border flex items-center p-1.5 text-[13px] cursor-pointer w-[167px]"
|
||||
@click="toggleAvatarCopying">
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<span class="block truncate font-medium leading-[18px]">{{
|
||||
t('dialog.user.info.avatar_cloning')
|
||||
}}</span>
|
||||
<span v-if="currentUser.allowAvatarCopying" class="block truncate text-xs">{{
|
||||
t('dialog.user.info.avatar_cloning_allow')
|
||||
}}</span>
|
||||
<span v-else class="block truncate text-xs">{{ t('dialog.user.info.avatar_cloning_deny') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="box-border flex items-center p-1.5 text-[13px] cursor-pointer w-[167px]"
|
||||
@click="toggleAllowBooping">
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<span class="block truncate font-medium leading-[18px]">{{ t('dialog.user.info.booping') }}</span>
|
||||
<span v-if="currentUser.isBoopingEnabled" class="block truncate text-xs">{{
|
||||
t('dialog.user.info.avatar_cloning_allow')
|
||||
}}</span>
|
||||
<span v-else class="block truncate text-xs">{{ t('dialog.user.info.avatar_cloning_deny') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="box-border flex items-center p-1.5 text-[13px] cursor-pointer w-[167px]"
|
||||
@click="toggleSharedConnectionsOptOut">
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<span class="block truncate font-medium leading-[18px]">{{
|
||||
t('dialog.user.info.show_mutual_friends')
|
||||
}}</span>
|
||||
<span v-if="!currentUser.hasSharedConnectionsOptOut" class="block truncate text-xs">{{
|
||||
t('dialog.user.info.avatar_cloning_allow')
|
||||
}}</span>
|
||||
<span v-else class="block truncate text-xs">{{ t('dialog.user.info.avatar_cloning_deny') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="box-border flex items-center p-1.5 text-[13px] cursor-pointer w-[167px]"
|
||||
@click="toggleDiscordFriendsOptOut">
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<span class="block truncate font-medium leading-[18px]">{{
|
||||
t('dialog.user.info.show_discord_connections')
|
||||
}}</span>
|
||||
<span v-if="!currentUser.hasDiscordFriendsOptOut" class="block truncate text-xs">{{
|
||||
t('dialog.user.info.avatar_cloning_allow')
|
||||
}}</span>
|
||||
<span v-else class="block truncate text-xs">{{ t('dialog.user.info.avatar_cloning_deny') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="box-border flex items-center p-1.5 text-[13px] cursor-default w-[167px]">
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<span class="block truncate font-medium leading-[18px]">{{
|
||||
t('dialog.user.info.avatar_cloning')
|
||||
}}</span>
|
||||
<span v-if="userDialog.ref.allowAvatarCopying" class="block truncate text-xs">{{
|
||||
t('dialog.user.info.avatar_cloning_allow')
|
||||
}}</span>
|
||||
<span v-else class="block truncate text-xs">{{ t('dialog.user.info.avatar_cloning_deny') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div
|
||||
v-if="userDialog.ref.id === currentUser.id"
|
||||
class="box-border flex items-center p-1.5 text-[13px] cursor-pointer w-[167px]"
|
||||
@click="getVRChatCredits()">
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<span class="block truncate font-medium leading-[18px]">{{
|
||||
t('view.profile.profile.vrchat_credits')
|
||||
}}</span>
|
||||
<span class="block truncate text-xs">{{ vrchatCredit ?? t('view.profile.profile.refresh') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="userDialog.ref.id === currentUser.id && currentUser.homeLocation"
|
||||
class="box-border flex items-center p-1.5 text-[13px] w-full cursor-pointer"
|
||||
@click="showWorldDialog(currentUser.homeLocation)">
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<span class="block truncate font-medium leading-[18px]">{{ t('dialog.user.info.home_location') }}</span>
|
||||
<span class="block truncate text-xs">
|
||||
<span v-text="userDialog.$homeLocationName"></span>
|
||||
<Button class="rounded-full ml-1 text-xs" size="icon-sm" variant="ghost" @click.stop="resetHome()"
|
||||
><Trash2 class="h-4 w-4" />
|
||||
</Button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-border flex items-center p-1.5 text-[13px] w-full cursor-default">
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<span class="block truncate font-medium leading-[18px]">{{ t('dialog.user.info.id') }}</span>
|
||||
<span class="block truncate text-xs">
|
||||
{{ userDialog.id }}
|
||||
<TooltipWrapper side="top" :content="t('dialog.user.info.id_tooltip')">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger as-child>
|
||||
<Button class="rounded-full ml-1 text-xs" size="icon-sm" variant="ghost" @click.stop
|
||||
><Copy class="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem @click="copyUserId(userDialog.id)">
|
||||
{{ t('dialog.user.info.copy_id') }}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem @click="copyUserURL(userDialog.id)">
|
||||
{{ t('dialog.user.info.copy_url') }}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem @click="copyUserDisplayName(userDialog.ref.displayName)">
|
||||
{{ t('dialog.user.info.copy_display_name') }}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TooltipWrapper>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<EditNoteAndMemoDialog v-model:visible="isEditNoteAndMemoDialogVisible" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Copy, Info, Languages, MoreHorizontal, Pencil, Trash2 } from 'lucide-vue-next';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
import { defineAsyncComponent, ref, watch } from 'vue';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Spinner } from '@/components/ui/spinner';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { toast } from 'vue-sonner';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import InstanceActionBar from '../../InstanceActionBar.vue';
|
||||
|
||||
import {
|
||||
copyToClipboard,
|
||||
formatDateFilter,
|
||||
getFaviconUrl,
|
||||
isFriendOnline,
|
||||
isRealInstance,
|
||||
openExternalLink,
|
||||
refreshInstancePlayerCount,
|
||||
timeToText,
|
||||
userImage,
|
||||
userOnlineFor,
|
||||
userOnlineForTimestamp,
|
||||
userStatusClass
|
||||
} from '../../../shared/utils';
|
||||
import {
|
||||
useAdvancedSettingsStore,
|
||||
useAppearanceSettingsStore,
|
||||
useGalleryStore,
|
||||
useGroupStore,
|
||||
useInstanceStore,
|
||||
useLocationStore,
|
||||
useModalStore,
|
||||
useUserStore,
|
||||
useWorldStore
|
||||
} from '../../../stores';
|
||||
import { miscRequest, userRequest } from '../../../api';
|
||||
|
||||
const EditNoteAndMemoDialog = defineAsyncComponent(() => import('./EditNoteAndMemoDialog.vue'));
|
||||
|
||||
defineEmits(['showBioDialog']);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const modalStore = useModalStore();
|
||||
const instanceStore = useInstanceStore();
|
||||
|
||||
const { hideUserNotes, hideUserMemos } = storeToRefs(useAppearanceSettingsStore());
|
||||
const { bioLanguage, translationApi, translationApiType } = storeToRefs(useAdvancedSettingsStore());
|
||||
const { translateText } = useAdvancedSettingsStore();
|
||||
const { userDialog, currentUser } = storeToRefs(useUserStore());
|
||||
const { showUserDialog, toggleSharedConnectionsOptOut, toggleDiscordFriendsOptOut } = useUserStore();
|
||||
const { showWorldDialog } = useWorldStore();
|
||||
const { showGroupDialog } = useGroupStore();
|
||||
const { lastLocation } = storeToRefs(useLocationStore());
|
||||
const { showFullscreenImageDialog } = useGalleryStore();
|
||||
|
||||
const bioCache = ref({
|
||||
userId: null,
|
||||
translated: null
|
||||
});
|
||||
|
||||
const isEditNoteAndMemoDialogVisible = ref(false);
|
||||
const vrchatCredit = ref(null);
|
||||
const translateLoading = ref(false);
|
||||
|
||||
watch(
|
||||
() => userDialog.value.loading,
|
||||
() => {
|
||||
if (userDialog.value.visible) {
|
||||
if (userDialog.value.id !== bioCache.value.userId) {
|
||||
bioCache.value = {
|
||||
userId: null,
|
||||
translated: null
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function onTabActivated() {
|
||||
if (vrchatCredit.value === null) {
|
||||
getVRChatCredits();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function showEditNoteAndMemoDialog() {
|
||||
isEditNoteAndMemoDialogVisible.value = true;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
async function translateBio() {
|
||||
if (translateLoading.value) {
|
||||
return;
|
||||
}
|
||||
const bio = userDialog.value.ref.bio;
|
||||
if (!bio) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetLang = bioLanguage.value;
|
||||
|
||||
if (bioCache.value.userId !== userDialog.value.id) {
|
||||
bioCache.value.userId = userDialog.value.id;
|
||||
bioCache.value.translated = null;
|
||||
}
|
||||
|
||||
if (bioCache.value.translated) {
|
||||
bioCache.value.translated = null;
|
||||
return;
|
||||
}
|
||||
|
||||
translateLoading.value = true;
|
||||
try {
|
||||
const providerLabel = translationApiType.value === 'openai' ? 'OpenAI' : 'Google';
|
||||
const translated = await translateText(`${bio}\n\nTranslated by ${providerLabel}`, targetLang);
|
||||
if (!translated) {
|
||||
throw new Error('No translation returned');
|
||||
}
|
||||
|
||||
bioCache.value.translated = translated;
|
||||
} catch (err) {
|
||||
console.error('Translation failed:', err);
|
||||
} finally {
|
||||
translateLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param userRef
|
||||
*/
|
||||
function showPreviousInstancesListDialog(userRef) {
|
||||
instanceStore.showPreviousInstancesListDialog('user', userRef);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function toggleAvatarCopying() {
|
||||
userRequest.saveCurrentUser({
|
||||
allowAvatarCopying: !currentUser.value.allowAvatarCopying
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function toggleAllowBooping() {
|
||||
userRequest.saveCurrentUser({
|
||||
isBoopingEnabled: !currentUser.value.isBoopingEnabled
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function resetHome() {
|
||||
modalStore
|
||||
.confirm({
|
||||
description: t('confirm.command_question', {
|
||||
command: t('dialog.user.actions.reset_home')
|
||||
}),
|
||||
title: t('confirm.title')
|
||||
})
|
||||
.then(({ ok }) => {
|
||||
if (!ok) return;
|
||||
userRequest
|
||||
.saveCurrentUser({
|
||||
homeLocation: ''
|
||||
})
|
||||
.then((args) => {
|
||||
toast.success('Home world has been reset');
|
||||
return args;
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param userId
|
||||
*/
|
||||
function copyUserId(userId) {
|
||||
copyToClipboard(userId, 'User ID copied to clipboard');
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param userId
|
||||
*/
|
||||
function copyUserURL(userId) {
|
||||
copyToClipboard(`https://vrchat.com/home/user/${userId}`, 'User URL copied to clipboard');
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param displayName
|
||||
*/
|
||||
function copyUserDisplayName(displayName) {
|
||||
copyToClipboard(displayName, 'User DisplayName copied to clipboard');
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function getVRChatCredits() {
|
||||
miscRequest.getVRChatCredits().then((args) => (vrchatCredit.value = args.json?.balance));
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
onTabActivated,
|
||||
showEditNoteAndMemoDialog
|
||||
});
|
||||
</script>
|
||||
@@ -4,15 +4,20 @@ import { mount } from '@vue/test-utils';
|
||||
|
||||
// ─── Mocks (must be before any imports that use them) ────────────────
|
||||
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: (key, params) => (params ? `${key}:${JSON.stringify(params)}` : key)
|
||||
}),
|
||||
createI18n: () => ({
|
||||
global: { t: (key) => key },
|
||||
install: vi.fn()
|
||||
})
|
||||
}));
|
||||
vi.mock('vue-i18n', () => {
|
||||
const { ref } = require('vue');
|
||||
return {
|
||||
useI18n: () => ({
|
||||
t: (key, params) =>
|
||||
params ? `${key}:${JSON.stringify(params)}` : key,
|
||||
locale: ref('en')
|
||||
}),
|
||||
createI18n: () => ({
|
||||
global: { t: (key) => key, locale: ref('en') },
|
||||
install: vi.fn()
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('../../../../plugin/router', () => {
|
||||
const { ref } = require('vue');
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { flushPromises, shallowMount } from '@vue/test-utils';
|
||||
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => {
|
||||
const { ref } = require('vue');
|
||||
return {
|
||||
t: (key, params) => (params ? `${key}:${JSON.stringify(params)}` : key),
|
||||
locale: ref('en')
|
||||
};
|
||||
},
|
||||
createI18n: () => ({
|
||||
global: { t: (key) => key },
|
||||
install: vi.fn()
|
||||
})
|
||||
}));
|
||||
|
||||
vi.mock('../../../../plugin/router', () => {
|
||||
const { ref } = require('vue');
|
||||
return {
|
||||
router: {
|
||||
beforeEach: vi.fn(),
|
||||
push: vi.fn(),
|
||||
replace: vi.fn(),
|
||||
currentRoute: ref({ path: '/', name: '', meta: {} }),
|
||||
isReady: vi.fn().mockResolvedValue(true)
|
||||
},
|
||||
initRouter: vi.fn()
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('vue-router', async (importOriginal) => {
|
||||
const actual = await importOriginal();
|
||||
const { ref } = require('vue');
|
||||
return {
|
||||
...actual,
|
||||
useRouter: vi.fn(() => ({
|
||||
push: vi.fn(),
|
||||
replace: vi.fn(),
|
||||
currentRoute: ref({ path: '/', name: '', meta: {} })
|
||||
}))
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('../../../../plugin/interopApi', () => ({ initInteropApi: vi.fn() }));
|
||||
vi.mock('../../../../service/database', () => ({
|
||||
database: new Proxy(
|
||||
{},
|
||||
{
|
||||
get: (_target, prop) => {
|
||||
if (prop === '__esModule') return false;
|
||||
return vi.fn().mockResolvedValue(null);
|
||||
}
|
||||
}
|
||||
)
|
||||
}));
|
||||
|
||||
vi.mock('../../../../service/config', () => ({
|
||||
default: {
|
||||
init: vi.fn(),
|
||||
getString: vi.fn().mockImplementation((_k, d) => d ?? '{}'),
|
||||
setString: vi.fn(),
|
||||
getBool: vi.fn().mockImplementation((_k, d) => d ?? false),
|
||||
setBool: vi.fn(),
|
||||
getInt: vi.fn().mockImplementation((_k, d) => d ?? 0),
|
||||
setInt: vi.fn(),
|
||||
getFloat: vi.fn().mockImplementation((_k, d) => d ?? 0),
|
||||
setFloat: vi.fn(),
|
||||
getObject: vi.fn().mockReturnValue(null),
|
||||
setObject: vi.fn(),
|
||||
getArray: vi.fn().mockReturnValue([]),
|
||||
setArray: vi.fn(),
|
||||
remove: vi.fn()
|
||||
}
|
||||
}));
|
||||
|
||||
vi.mock('../../../../service/jsonStorage', () => ({ default: vi.fn() }));
|
||||
vi.mock('../../../../service/watchState', () => ({
|
||||
watchState: { isLoggedIn: false }
|
||||
}));
|
||||
vi.mock('../../../../service/request', () => ({
|
||||
request: vi.fn().mockResolvedValue({ json: {} }),
|
||||
processBulk: vi.fn(),
|
||||
buildRequestInit: vi.fn(),
|
||||
parseResponse: vi.fn(),
|
||||
shouldIgnoreError: vi.fn(),
|
||||
$throw: vi.fn(),
|
||||
failedGetRequests: new Map()
|
||||
}));
|
||||
|
||||
import UserDialogInfoTab from '../UserDialogInfoTab.vue';
|
||||
import { miscRequest } from '../../../../api';
|
||||
import {
|
||||
useAdvancedSettingsStore,
|
||||
useAppearanceSettingsStore,
|
||||
useLocationStore,
|
||||
useModalStore,
|
||||
useUserStore
|
||||
} from '../../../../stores';
|
||||
|
||||
/**
|
||||
*
|
||||
* @param overrides
|
||||
*/
|
||||
function mountComponent(overrides = {}) {
|
||||
const pinia = createTestingPinia({
|
||||
stubActions: true
|
||||
});
|
||||
|
||||
const appearanceSettingsStore = useAppearanceSettingsStore(pinia);
|
||||
appearanceSettingsStore.hideUserNotes = false;
|
||||
appearanceSettingsStore.hideUserMemos = false;
|
||||
|
||||
const advancedSettingsStore = useAdvancedSettingsStore(pinia);
|
||||
advancedSettingsStore.bioLanguage = 'en';
|
||||
advancedSettingsStore.translationApi = '';
|
||||
advancedSettingsStore.translationApiType = 'google';
|
||||
advancedSettingsStore.translateText = vi.fn().mockResolvedValue('');
|
||||
|
||||
const userStore = useUserStore(pinia);
|
||||
userStore.userDialog = {
|
||||
id: 'usr_target',
|
||||
friend: {
|
||||
state: 'online',
|
||||
ref: {
|
||||
location: 'wrld_test:123'
|
||||
}
|
||||
},
|
||||
ref: {
|
||||
id: 'usr_target',
|
||||
location: 'wrld_test:123',
|
||||
travelingToLocation: '',
|
||||
profilePicOverride: '',
|
||||
currentAvatarImageUrl: '',
|
||||
currentAvatarTags: [],
|
||||
bio: '',
|
||||
bioLinks: [],
|
||||
state: 'online',
|
||||
$online_for: 1000,
|
||||
last_login: '2025-01-01T00:00:00.000Z',
|
||||
last_activity: '2025-01-01T00:00:00.000Z',
|
||||
date_joined: '2020-01-01',
|
||||
allowAvatarCopying: true,
|
||||
displayName: 'Target'
|
||||
},
|
||||
$location: {
|
||||
tag: 'wrld_test:123',
|
||||
shortName: 'Test',
|
||||
userId: '',
|
||||
user: null
|
||||
},
|
||||
instance: {
|
||||
ref: {},
|
||||
friendCount: 0
|
||||
},
|
||||
users: [
|
||||
{
|
||||
id: 'usr_friend_1',
|
||||
displayName: 'Friend A',
|
||||
$userColour: '#ffffff',
|
||||
location: 'traveling',
|
||||
$travelingToTime: Date.now(),
|
||||
$location_at: Date.now()
|
||||
}
|
||||
],
|
||||
note: '',
|
||||
memo: '',
|
||||
isRepresentedGroupLoading: false,
|
||||
representedGroup: null,
|
||||
lastSeen: '2025-01-01T00:00:00.000Z',
|
||||
joinCount: 0,
|
||||
timeSpent: 0,
|
||||
dateFriendedInfo: [],
|
||||
unFriended: false,
|
||||
dateFriended: '2025-01-01T00:00:00.000Z',
|
||||
$homeLocationName: '',
|
||||
...overrides.userDialog
|
||||
};
|
||||
|
||||
userStore.currentUser = {
|
||||
id: 'usr_me',
|
||||
allowAvatarCopying: true,
|
||||
isBoopingEnabled: true,
|
||||
hasSharedConnectionsOptOut: false,
|
||||
hasDiscordFriendsOptOut: false,
|
||||
homeLocation: '',
|
||||
...overrides.currentUser
|
||||
};
|
||||
|
||||
const locationStore = useLocationStore(pinia);
|
||||
locationStore.lastLocation = {
|
||||
location: 'wrld_test:123'
|
||||
};
|
||||
|
||||
const modalStore = useModalStore(pinia);
|
||||
modalStore.confirm = vi.fn().mockResolvedValue({ ok: false });
|
||||
|
||||
return shallowMount(UserDialogInfoTab, {
|
||||
global: {
|
||||
plugins: [pinia],
|
||||
stubs: {
|
||||
Location: true,
|
||||
Timer: true,
|
||||
TooltipWrapper: true,
|
||||
AvatarInfo: true
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
describe('UserDialogInfoTab.vue', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('unit behavior', () => {
|
||||
test('onTabActivated fetches VRChat credits only once after first success', async () => {
|
||||
const creditsSpy = vi
|
||||
.spyOn(miscRequest, 'getVRChatCredits')
|
||||
.mockResolvedValue({ json: { balance: 42 } });
|
||||
const wrapper = mountComponent();
|
||||
|
||||
wrapper.vm.onTabActivated();
|
||||
await flushPromises();
|
||||
wrapper.vm.onTabActivated();
|
||||
await flushPromises();
|
||||
|
||||
expect(creditsSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('dom rendering', () => {
|
||||
test('renders imported InstanceActionBar and Spinner components when conditions are met', () => {
|
||||
const wrapper = mountComponent();
|
||||
|
||||
expect(wrapper.find('instance-action-bar-stub').exists()).toBe(true);
|
||||
expect(wrapper.find('spinner-stub').exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -6,10 +6,11 @@ import { mount } from '@vue/test-utils';
|
||||
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: (key, params) => (params ? `${key}:${JSON.stringify(params)}` : key)
|
||||
t: (key, params) => (params ? `${key}:${JSON.stringify(params)}` : key),
|
||||
locale: require('vue').ref('en')
|
||||
}),
|
||||
createI18n: () => ({
|
||||
global: { t: (key) => key },
|
||||
global: { t: (key) => key , locale: require('vue').ref('en') },
|
||||
install: vi.fn()
|
||||
})
|
||||
}));
|
||||
|
||||
@@ -4,15 +4,20 @@ import { mount } from '@vue/test-utils';
|
||||
|
||||
// ─── Mocks ───────────────────────────────────────────────────────────
|
||||
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: (key, params) => (params ? `${key}:${JSON.stringify(params)}` : key)
|
||||
}),
|
||||
createI18n: () => ({
|
||||
global: { t: (key) => key },
|
||||
install: vi.fn()
|
||||
})
|
||||
}));
|
||||
vi.mock('vue-i18n', () => {
|
||||
const { ref } = require('vue');
|
||||
return {
|
||||
useI18n: () => ({
|
||||
t: (key, params) =>
|
||||
params ? `${key}:${JSON.stringify(params)}` : key,
|
||||
locale: ref('en')
|
||||
}),
|
||||
createI18n: () => ({
|
||||
global: { t: (key) => key, locale: ref('en') },
|
||||
install: vi.fn()
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('../../../../plugin/router', () => {
|
||||
const { ref } = require('vue');
|
||||
|
||||
@@ -5,7 +5,9 @@ vi.mock('vue-sonner', () => ({
|
||||
}));
|
||||
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({ t: (key) => key })
|
||||
useI18n: () => ({ t: (key) => key ,
|
||||
locale: require('vue').ref('en')
|
||||
})
|
||||
}));
|
||||
|
||||
import {
|
||||
|
||||
@@ -39,7 +39,9 @@ vi.mock('../../shared/utils', () => ({
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: (key) => key
|
||||
})
|
||||
,
|
||||
locale: require('vue').ref('en')
|
||||
})
|
||||
}));
|
||||
|
||||
/**
|
||||
|
||||
@@ -29,7 +29,9 @@ vi.mock('vue-sonner', () => ({
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: (key) => key
|
||||
})
|
||||
,
|
||||
locale: require('vue').ref('en')
|
||||
})
|
||||
}));
|
||||
|
||||
function flushPromises() {
|
||||
|
||||
@@ -213,8 +213,9 @@ export const useFavoriteStore = defineStore('Favorite', () => {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param list
|
||||
* @param selectionRef
|
||||
* @param {Array} list
|
||||
* @param {object} selectionRef
|
||||
* @returns {void}
|
||||
*/
|
||||
function syncFavoriteSelection(list, selectionRef) {
|
||||
if (!Array.isArray(list)) {
|
||||
@@ -263,7 +264,7 @@ export const useFavoriteStore = defineStore('Favorite', () => {
|
||||
);
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
function getCachedFavoriteGroupsByTypeName() {
|
||||
const group = {};
|
||||
@@ -286,7 +287,8 @@ export const useFavoriteStore = defineStore('Favorite', () => {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param objectId
|
||||
* @param {string} objectId
|
||||
* @returns {object | undefined}
|
||||
*/
|
||||
function getCachedFavoritesByObjectId(objectId) {
|
||||
return cachedFavoritesByObjectId.get(objectId);
|
||||
@@ -294,7 +296,8 @@ export const useFavoriteStore = defineStore('Favorite', () => {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param args
|
||||
* @param {object} args
|
||||
* @returns {void}
|
||||
*/
|
||||
function handleFavoriteAdd(args) {
|
||||
handleFavorite({
|
||||
@@ -330,7 +333,8 @@ export const useFavoriteStore = defineStore('Favorite', () => {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param args
|
||||
* @param {object} args
|
||||
* @returns {void}
|
||||
*/
|
||||
function handleFavorite(args) {
|
||||
args.ref = applyFavoriteCached(args.json);
|
||||
@@ -353,7 +357,8 @@ export const useFavoriteStore = defineStore('Favorite', () => {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param objectId
|
||||
* @param {string} objectId
|
||||
* @returns {void}
|
||||
*/
|
||||
function handleFavoriteDelete(objectId) {
|
||||
const ref = getCachedFavoritesByObjectId(objectId);
|
||||
@@ -365,7 +370,8 @@ export const useFavoriteStore = defineStore('Favorite', () => {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param args
|
||||
* @param {object} args
|
||||
* @returns {void}
|
||||
*/
|
||||
function handleFavoriteGroup(args) {
|
||||
args.ref = applyFavoriteGroup(args.json);
|
||||
@@ -373,7 +379,8 @@ export const useFavoriteStore = defineStore('Favorite', () => {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param args
|
||||
* @param {object} args
|
||||
* @returns {void}
|
||||
*/
|
||||
function handleFavoriteGroupClear(args) {
|
||||
const key = `${args.params.type}:${args.params.group}`;
|
||||
@@ -387,7 +394,8 @@ export const useFavoriteStore = defineStore('Favorite', () => {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param args
|
||||
* @param {object} args
|
||||
* @returns {void}
|
||||
*/
|
||||
function handleFavoriteWorldList(args) {
|
||||
for (const json of args.json) {
|
||||
@@ -400,7 +408,7 @@ export const useFavoriteStore = defineStore('Favorite', () => {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param args
|
||||
* @param {object} args
|
||||
*/
|
||||
function handleFavoriteAvatarList(args) {
|
||||
for (const json of args.json) {
|
||||
@@ -413,7 +421,8 @@ export const useFavoriteStore = defineStore('Favorite', () => {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param ref
|
||||
* @param {object} ref
|
||||
* @returns {void}
|
||||
*/
|
||||
function handleFavoriteAtDelete(ref) {
|
||||
const favorite = state.favoriteObjects.get(ref.favoriteId);
|
||||
@@ -583,7 +592,7 @@ export const useFavoriteStore = defineStore('Favorite', () => {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
function refreshFavoriteGroups() {
|
||||
if (isFavoriteGroupLoading.value) {
|
||||
@@ -813,8 +822,8 @@ export const useFavoriteStore = defineStore('Favorite', () => {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param json
|
||||
* @returns {any}
|
||||
* @param {object} json
|
||||
* @returns {object}
|
||||
*/
|
||||
function applyFavoriteGroup(json) {
|
||||
let ref = cachedFavoriteGroups.value[json.id];
|
||||
@@ -829,8 +838,8 @@ export const useFavoriteStore = defineStore('Favorite', () => {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param json
|
||||
* @returns {any}
|
||||
* @param {object} json
|
||||
* @returns {object}
|
||||
*/
|
||||
function applyFavoriteCached(json) {
|
||||
let ref = cachedFavorites.get(json.id);
|
||||
@@ -866,7 +875,8 @@ export const useFavoriteStore = defineStore('Favorite', () => {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param tag
|
||||
* @param {string} tag
|
||||
* @returns {void}
|
||||
*/
|
||||
async function refreshFavoriteAvatars(tag) {
|
||||
const params = {
|
||||
@@ -879,7 +889,7 @@ export const useFavoriteStore = defineStore('Favorite', () => {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
function refreshFavoriteItems() {
|
||||
const types = {
|
||||
@@ -929,21 +939,21 @@ export const useFavoriteStore = defineStore('Favorite', () => {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
function showWorldImportDialog() {
|
||||
worldImportDialogVisible.value = true;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
function showAvatarImportDialog() {
|
||||
avatarImportDialogVisible.value = true;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
function showFriendImportDialog() {
|
||||
friendImportDialogVisible.value = true;
|
||||
@@ -1104,7 +1114,8 @@ export const useFavoriteStore = defineStore('Favorite', () => {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param objectId
|
||||
* @param {string} objectId
|
||||
* @returns {void}
|
||||
*/
|
||||
function updateFavoriteDialog(objectId) {
|
||||
const D = favoriteDialog.value;
|
||||
@@ -1199,7 +1210,7 @@ export const useFavoriteStore = defineStore('Favorite', () => {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
function sortLocalAvatarFavorites() {
|
||||
if (!appearanceSettingsStore.sortFavorites) {
|
||||
@@ -1389,7 +1400,7 @@ export const useFavoriteStore = defineStore('Favorite', () => {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
function sortLocalWorldFavorites() {
|
||||
if (!appearanceSettingsStore.sortFavorites) {
|
||||
@@ -1450,6 +1461,10 @@ export const useFavoriteStore = defineStore('Favorite', () => {
|
||||
});
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`Failed to fetch avatar ${favorite.id}:`,
|
||||
err
|
||||
);
|
||||
result.invalid++;
|
||||
result.invalidIds.push(favorite.id);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,9 @@ vi.mock('pinia', () => ({
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: (key) => key
|
||||
})
|
||||
,
|
||||
locale: require('vue').ref('en')
|
||||
})
|
||||
}));
|
||||
|
||||
vi.mock('@/components/ui/context-menu', () => ({
|
||||
|
||||
@@ -46,7 +46,8 @@ vi.mock('pinia', () => ({
|
||||
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: (key) => key
|
||||
t: (key) => key,
|
||||
locale: require('vue').ref('en')
|
||||
})
|
||||
}));
|
||||
|
||||
|
||||
@@ -34,7 +34,9 @@ vi.mock('pinia', () => ({
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: (key) => key
|
||||
})
|
||||
,
|
||||
locale: require('vue').ref('en')
|
||||
})
|
||||
}));
|
||||
|
||||
vi.mock('../../../stores', () => ({
|
||||
|
||||
@@ -44,7 +44,8 @@ vi.mock('pinia', () => ({
|
||||
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: (key) => key
|
||||
t: (key) => key,
|
||||
locale: require('vue').ref('en')
|
||||
})
|
||||
}));
|
||||
|
||||
|
||||
@@ -19,7 +19,9 @@ vi.mock('pinia', () => ({
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: (key) => key
|
||||
})
|
||||
,
|
||||
locale: require('vue').ref('en')
|
||||
})
|
||||
}));
|
||||
|
||||
vi.mock('../../../../stores', () => ({
|
||||
|
||||
@@ -32,7 +32,9 @@ vi.mock('pinia', () => ({
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: (key) => key
|
||||
})
|
||||
,
|
||||
locale: require('vue').ref('en')
|
||||
})
|
||||
}));
|
||||
|
||||
vi.mock('../../../stores', () => ({
|
||||
|
||||
@@ -28,7 +28,9 @@ vi.mock('pinia', () => ({
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: (key) => key
|
||||
})
|
||||
,
|
||||
locale: require('vue').ref('en')
|
||||
})
|
||||
}));
|
||||
|
||||
vi.mock('../../../stores', () => ({
|
||||
|
||||
@@ -20,7 +20,9 @@ vi.mock('pinia', () => ({
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: (key) => key
|
||||
})
|
||||
,
|
||||
locale: require('vue').ref('en')
|
||||
})
|
||||
}));
|
||||
|
||||
vi.mock('../../../../stores', () => ({
|
||||
|
||||
@@ -27,7 +27,9 @@ vi.mock('pinia', () => ({
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: (key) => key
|
||||
})
|
||||
,
|
||||
locale: require('vue').ref('en')
|
||||
})
|
||||
}));
|
||||
|
||||
vi.mock('../../../../stores', () => ({
|
||||
|
||||
@@ -42,7 +42,8 @@ vi.mock('pinia', () => ({
|
||||
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: (key, params) => (params ? `${key}:${JSON.stringify(params)}` : key)
|
||||
t: (key, params) => (params ? `${key}:${JSON.stringify(params)}` : key),
|
||||
locale: require('vue').ref('en')
|
||||
})
|
||||
}));
|
||||
|
||||
|
||||
@@ -49,7 +49,8 @@ vi.mock('pinia', () => ({
|
||||
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: (key, params) => (params ? `${key}:${JSON.stringify(params)}` : key)
|
||||
t: (key, params) => (params ? `${key}:${JSON.stringify(params)}` : key),
|
||||
locale: require('vue').ref('en')
|
||||
})
|
||||
}));
|
||||
|
||||
|
||||
@@ -33,7 +33,9 @@ vi.mock('../../../../shared/utils', () => ({
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: (key) => key
|
||||
})
|
||||
,
|
||||
locale: require('vue').ref('en')
|
||||
})
|
||||
}));
|
||||
|
||||
vi.mock('@/components/ui/avatar', () => ({
|
||||
|
||||
@@ -150,7 +150,8 @@ vi.mock('vue-sonner', () => ({
|
||||
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: (key) => key
|
||||
t: (key) => key,
|
||||
locale: require('vue').ref('en')
|
||||
})
|
||||
}));
|
||||
|
||||
|
||||
@@ -26,7 +26,9 @@ vi.mock('vue-router', () => ({
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: (key) => key
|
||||
})
|
||||
,
|
||||
locale: require('vue').ref('en')
|
||||
})
|
||||
}));
|
||||
|
||||
vi.mock('../../../../stores', () => ({
|
||||
|
||||
@@ -52,7 +52,8 @@ vi.mock('../../../../shared/utils', () => ({
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: (key) => key,
|
||||
te: () => false
|
||||
te: () => false,
|
||||
locale: require('vue').ref('en')
|
||||
})
|
||||
}));
|
||||
|
||||
@@ -72,13 +73,19 @@ vi.mock('@/components/ui/avatar', () => ({
|
||||
props: ['src'],
|
||||
template: '<img data-testid="avatar-image" :src="src" />'
|
||||
},
|
||||
AvatarFallback: { template: '<span data-testid="avatar-fallback"><slot /></span>' }
|
||||
AvatarFallback: {
|
||||
template: '<span data-testid="avatar-fallback"><slot /></span>'
|
||||
}
|
||||
}));
|
||||
|
||||
vi.mock('@/components/ui/hover-card', () => ({
|
||||
HoverCard: { template: '<div data-testid="hover-card"><slot /></div>' },
|
||||
HoverCardTrigger: { template: '<div data-testid="hover-trigger"><slot /></div>' },
|
||||
HoverCardContent: { template: '<div data-testid="hover-content"><slot /></div>' }
|
||||
HoverCardTrigger: {
|
||||
template: '<div data-testid="hover-trigger"><slot /></div>'
|
||||
},
|
||||
HoverCardContent: {
|
||||
template: '<div data-testid="hover-content"><slot /></div>'
|
||||
}
|
||||
}));
|
||||
|
||||
vi.mock('@/components/ui/badge', () => ({
|
||||
@@ -197,9 +204,9 @@ describe('NotificationItem.vue', () => {
|
||||
});
|
||||
|
||||
await wrapper.get('[data-icon="Link"]').trigger('click');
|
||||
expect(mocks.notificationStore.openNotificationLink).toHaveBeenCalledWith(
|
||||
'group:grp_123'
|
||||
);
|
||||
expect(
|
||||
mocks.notificationStore.openNotificationLink
|
||||
).toHaveBeenCalledWith('group:grp_123');
|
||||
});
|
||||
|
||||
test('unmount queues mark-as-seen for unseen notification', () => {
|
||||
|
||||
@@ -22,7 +22,9 @@ vi.mock('@tanstack/vue-virtual', () => ({
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: (key) => key
|
||||
})
|
||||
,
|
||||
locale: require('vue').ref('en')
|
||||
})
|
||||
}));
|
||||
|
||||
vi.mock('@/components/ui/button', () => ({
|
||||
|
||||
Reference in New Issue
Block a user