diff --git a/src/components/NavMenu.vue b/src/components/NavMenu.vue
index 805f18f8..9ce96251 100644
--- a/src/components/NavMenu.vue
+++ b/src/components/NavMenu.vue
@@ -8,7 +8,7 @@
{{
item.titleIsCustom ? item.title : t(item.title || '')
}}
+
+ {{ isMac ? '⌘' : 'Ctrl' }}
+ D
+
@@ -333,9 +337,10 @@
DropdownMenuSubTrigger,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu';
- import { computed, defineAsyncComponent, onMounted, ref, watch } from 'vue';
+ import { computed, defineAsyncComponent, h, onMounted, ref, watch } from 'vue';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
import { ChevronRight, Heart } from 'lucide-vue-next';
+ import { Kbd } from '@/components/ui/kbd';
import { TooltipWrapper } from '@/components/ui/tooltip';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
@@ -361,6 +366,21 @@
const { t, locale } = useI18n();
const router = useRouter();
+ const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
+
+ const getItemTooltip = (item) => {
+ const label = item.titleIsCustom ? item.title : t(item.title || '');
+ if (item.action !== 'direct-access') {
+ return label;
+ }
+ return () =>
+ h('span', { class: 'inline-flex items-center gap-1' }, [
+ label,
+ h(Kbd, () => (isMac ? '⌘' : 'Ctrl')),
+ h(Kbd, () => 'D')
+ ]);
+ };
+
const createDefaultNavLayout = () => [
{ type: 'item', key: 'feed' },
{ type: 'item', key: 'friends-locations' },
@@ -804,6 +824,10 @@
router.push({ name: routeName });
};
+ /**
+ *
+ * @param layout
+ */
function getFirstNavRoute(layout) {
for (const entry of layout) {
if (entry.type === 'item') {
diff --git a/src/components/dialogs/UserDialog/UserDialog.vue b/src/components/dialogs/UserDialog/UserDialog.vue
index b0b721db..2d3cde70 100644
--- a/src/components/dialogs/UserDialog/UserDialog.vue
+++ b/src/components/dialogs/UserDialog/UserDialog.vue
@@ -1501,6 +1501,10 @@
}
);
+ /**
+ *
+ * @param visibility
+ */
function userFavoriteWorldsStatus(visibility) {
const style = {};
if (visibility === 'public') {
@@ -1513,6 +1517,10 @@
return style;
}
+ /**
+ *
+ * @param user
+ */
function getUserStateText(user) {
let state = '';
if (user.state === 'active') {
@@ -1528,6 +1536,10 @@
return state;
}
+ /**
+ *
+ * @param status
+ */
function getUserStatusText(status) {
if (status === 'active') {
return t('dialog.user.status.active');
@@ -1544,6 +1556,9 @@
return t('dialog.user.status.offline');
}
+ /**
+ *
+ */
function refreshUserDialogTreeData() {
const D = userDialog.value;
if (D.id === currentUser.value.id) {
@@ -1556,6 +1571,10 @@
treeData.value = formatJsonVars(D.ref);
}
+ /**
+ *
+ * @param tabName
+ */
function handleUserDialogTab(tabName) {
userDialog.value.lastActiveTab = tabName;
const userId = userDialog.value.id;
@@ -1604,10 +1623,17 @@
}
}
+ /**
+ *
+ */
function loadLastActiveTab() {
handleUserDialogTab(userDialog.value.lastActiveTab);
}
+ /**
+ *
+ * @param tabName
+ */
function userDialogTabClick(tabName) {
if (tabName === userDialog.value.lastActiveTab) {
if (tabName === 'JSON') {
@@ -1618,17 +1644,26 @@
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;
}
+ /**
+ *
+ */
function showSocialStatusDialog() {
const D = socialStatusDialog.value;
const { statusHistory } = currentUser.value;
@@ -1646,6 +1681,10 @@
D.visible = true;
}
+ /**
+ *
+ * @param userId
+ */
async function setUserDialogAvatarsRemote(userId) {
if (avatarRemoteDatabase.value && userId !== currentUser.value.id) {
userDialog.value.isAvatarsLoading = true;
@@ -1672,6 +1711,10 @@
sortUserDialogAvatars(userDialog.value.avatars);
}
+ /**
+ *
+ * @param badge
+ */
async function toggleBadgeVisibility(badge) {
if (badge.hidden) {
badge.showcased = false;
@@ -1684,6 +1727,10 @@
handleBadgeUpdate(args);
}
+ /**
+ *
+ * @param badge
+ */
async function toggleBadgeShowcased(badge) {
if (badge.showcased) {
badge.hidden = false;
@@ -1696,12 +1743,21 @@
handleBadgeUpdate(args);
}
+ /**
+ *
+ * @param args
+ */
function handleBadgeUpdate(args) {
if (args.json) {
toast.success(t('message.badge.updated'));
}
}
+ /**
+ *
+ * @param userId
+ * @param type
+ */
function setPlayerModeration(userId, type) {
const D = userDialog.value;
AppApi.SetVRChatUserModeration(currentUser.value.id, userId, type).then((result) => {
@@ -1722,6 +1778,11 @@
});
}
+ /**
+ *
+ * @param params
+ * @param userId
+ */
function showSendInviteDialog(params, userId) {
sendInviteDialog.value = {
params,
@@ -1733,6 +1794,11 @@
sendInviteDialogVisible.value = true;
}
+ /**
+ *
+ * @param params
+ * @param userId
+ */
function showSendInviteRequestDialog(params, userId) {
sendInviteDialog.value = {
params,
@@ -1744,12 +1810,21 @@
sendInviteRequestDialogVisible.value = true;
}
+ /**
+ *
+ * @param groupId
+ * @param userId
+ */
function showInviteGroupDialog(groupId, userId) {
inviteGroupDialog.value.groupId = groupId;
inviteGroupDialog.value.userId = userId;
inviteGroupDialog.value.visible = true;
}
+ /**
+ *
+ * @param command
+ */
function userDialogCommand(command) {
let L;
const D = userDialog.value;
@@ -1892,6 +1967,10 @@
}
}
+ /**
+ *
+ * @param args
+ */
function handleSendFriendRequest(args) {
const ref = cachedUsers.get(args.params.userId);
if (typeof ref === 'undefined') {
@@ -1917,6 +1996,10 @@
}
}
+ /**
+ *
+ * @param args
+ */
function handleCancelFriendRequest(args) {
const ref = cachedUsers.get(args.params.userId);
if (typeof ref === 'undefined') {
@@ -1937,6 +2020,10 @@
D.outgoingRequest = false;
}
+ /**
+ *
+ * @param args
+ */
function handleSendPlayerModeration(args) {
const ref = applyPlayerModeration(args.json);
const D = userDialog.value;
@@ -1955,6 +2042,11 @@
toast.success(t('message.user.moderated'));
}
+ /**
+ *
+ * @param command
+ * @param userId
+ */
async function performUserDialogCommand(command, userId) {
let args;
let key;
@@ -2092,6 +2184,10 @@
}
}
+ /**
+ *
+ * @param userId
+ */
function reportUserForHacking(userId) {
miscRequest.reportUser({
userId,
@@ -2101,6 +2197,10 @@
});
}
+ /**
+ *
+ * @param userId
+ */
async function getUserGroups(userId) {
exitEditModeCurrentUserGroups();
userDialog.value.isGroupsLoading = true;
@@ -2161,6 +2261,10 @@
userDialog.value.isGroupsLoading = false;
}
+ /**
+ *
+ * @param userId
+ */
async function getUserMutualFriends(userId) {
userDialog.value.mutualFriends = [];
if (currentUser.value.hasSharedConnectionsOptOut) {
@@ -2200,6 +2304,11 @@
});
}
+ /**
+ *
+ * @param a
+ * @param b
+ */
function sortGroupsByInGame(a, b) {
const aIndex = inGameGroupOrder.value.indexOf(a?.id);
const bIndex = inGameGroupOrder.value.indexOf(b?.id);
@@ -2215,6 +2324,9 @@
return aIndex - bIndex;
}
+ /**
+ *
+ */
async function sortCurrentUserGroups() {
const D = userDialog.value;
let sortMethod = () => 0;
@@ -2237,6 +2349,10 @@
userDialog.value.userGroups.remainingGroups.sort(sortMethod);
}
+ /**
+ *
+ * @param userId
+ */
function setUserDialogAvatars(userId) {
const avatars = new Set();
userDialogAvatars.value.forEach((avatar) => {
@@ -2250,6 +2366,10 @@
sortUserDialogAvatars(userDialog.value.avatars);
}
+ /**
+ *
+ * @param userId
+ */
function setUserDialogWorlds(userId) {
const worlds = [];
for (const ref of cachedWorlds.values()) {
@@ -2260,6 +2380,9 @@
userDialog.value.worlds = worlds;
}
+ /**
+ *
+ */
function refreshUserDialogWorlds() {
const D = userDialog.value;
if (D.isWorldsLoading) {
@@ -2306,6 +2429,10 @@
});
}
+ /**
+ *
+ * @param userId
+ */
async function getUserFavoriteWorlds(userId) {
userDialog.value.isFavoriteWorldsLoading = true;
favoriteWorldsTab.value = '0';
@@ -2344,6 +2471,9 @@
userDialog.value.isFavoriteWorldsLoading = false;
}
+ /**
+ *
+ */
function showBioDialog() {
const D = bioDialog.value;
D.bio = currentUser.value.bio;
@@ -2351,6 +2481,9 @@
D.visible = true;
}
+ /**
+ *
+ */
async function translateBio() {
if (translateLoading.value) {
return;
@@ -2388,22 +2521,35 @@
}
}
+ /**
+ *
+ * @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({
@@ -2426,14 +2572,26 @@
.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');
}
@@ -2449,6 +2607,10 @@
return found ? String(found[0]) : '';
});
+ /**
+ *
+ * @param key
+ */
function setUserDialogGroupSortingByKey(key) {
const option = userDialogGroupSortingOptions[key];
if (!option) {
@@ -2457,6 +2619,10 @@
setUserDialogGroupSorting(option);
}
+ /**
+ *
+ * @param sortOrder
+ */
async function setUserDialogGroupSorting(sortOrder) {
const D = userDialog.value;
if (D.groupSorting.value === sortOrder.value) {
@@ -2477,6 +2643,10 @@
return found ? String(found[0]) : '';
});
+ /**
+ *
+ * @param key
+ */
function setUserDialogMutualFriendSortingByKey(key) {
const option = userDialogMutualFriendSortingOptions[key];
if (!option) {
@@ -2485,6 +2655,10 @@
setUserDialogMutualFriendSorting(option);
}
+ /**
+ *
+ * @param sortOrder
+ */
async function setUserDialogMutualFriendSorting(sortOrder) {
const D = userDialog.value;
D.mutualFriendSorting = sortOrder;
@@ -2501,6 +2675,9 @@
}
}
+ /**
+ *
+ */
async function exitEditModeCurrentUserGroups() {
userDialogGroupEditMode.value = false;
userDialogGroupEditGroups.value = [];
@@ -2509,6 +2686,9 @@
await sortCurrentUserGroups();
}
+ /**
+ *
+ */
async function editModeCurrentUserGroups() {
await updateInGameGroupOrder();
userDialogGroupEditGroups.value = Array.from(currentUserGroups.value.values());
@@ -2516,6 +2696,9 @@
userDialogGroupEditMode.value = true;
}
+ /**
+ *
+ */
async function saveInGameGroupOrder() {
userDialogGroupEditGroups.value.sort(sortGroupsByInGame);
try {
@@ -2531,6 +2714,9 @@
}
// Select all groups currently in the editable list by collecting their IDs
+ /**
+ *
+ */
function selectAllGroups() {
const allSelected = userDialogGroupEditSelectedGroupIds.value.length === userDialogGroupEditGroups.value.length;
@@ -2547,6 +2733,10 @@
const bulkGroupActionValue = ref('');
+ /**
+ *
+ * @param value
+ */
function handleBulkGroupAction(value) {
bulkGroupActionValue.value = value;
@@ -2563,6 +2753,10 @@
}
// Apply the given visibility to all selected groups
+ /**
+ *
+ * @param newVisibility
+ */
async function bulkSetVisibility(newVisibility) {
for (const groupId of userDialogGroupEditSelectedGroupIds.value) {
setGroupVisibility(groupId, newVisibility);
@@ -2570,6 +2764,9 @@
}
// Leave (remove user from) all selected groups
+ /**
+ *
+ */
function bulkLeaveGroups() {
for (const groupId of userDialogGroupEditSelectedGroupIds.value) {
leaveGroup(groupId);
@@ -2577,6 +2774,10 @@
}
// Toggle individual group selection for bulk actions
+ /**
+ *
+ * @param groupId
+ */
function toggleGroupSelection(groupId) {
const index = userDialogGroupEditSelectedGroupIds.value.indexOf(groupId);
if (index === -1) {
@@ -2586,6 +2787,10 @@
}
}
+ /**
+ *
+ * @param groupId
+ */
function moveGroupUp(groupId) {
const index = inGameGroupOrder.value.indexOf(groupId);
if (index > 0) {
@@ -2595,6 +2800,10 @@
}
}
+ /**
+ *
+ * @param groupId
+ */
function moveGroupDown(groupId) {
const index = inGameGroupOrder.value.indexOf(groupId);
if (index < inGameGroupOrder.value.length - 1) {
@@ -2604,6 +2813,10 @@
}
}
+ /**
+ *
+ * @param groupId
+ */
function moveGroupTop(groupId) {
const index = inGameGroupOrder.value.indexOf(groupId);
if (index > 0) {
@@ -2613,6 +2826,10 @@
}
}
+ /**
+ *
+ * @param groupId
+ */
function moveGroupBottom(groupId) {
const index = inGameGroupOrder.value.indexOf(groupId);
if (index < inGameGroupOrder.value.length - 1) {
@@ -2622,6 +2839,10 @@
}
}
+ /**
+ *
+ * @param sortOrder
+ */
async function setUserDialogWorldSorting(sortOrder) {
const D = userDialog.value;
if (D.worldSorting.value === sortOrder.value) {
@@ -2642,6 +2863,10 @@
return found ? String(found[0]) : '';
});
+ /**
+ *
+ * @param key
+ */
function setUserDialogWorldSortingByKey(key) {
const option = userDialogWorldSortingOptions[key];
if (!option) {
@@ -2650,6 +2875,10 @@
setUserDialogWorldSorting(option);
}
+ /**
+ *
+ * @param order
+ */
async function setUserDialogWorldOrder(order) {
const D = userDialog.value;
if (D.worldOrder.value === order.value) {
@@ -2670,6 +2899,10 @@
return found ? String(found[0]) : '';
});
+ /**
+ *
+ * @param key
+ */
function setUserDialogWorldOrderByKey(key) {
const option = userDialogWorldOrderOptions[key];
if (!option) {
@@ -2678,16 +2911,26 @@
setUserDialogWorldOrder(option);
}
+ /**
+ *
+ * @param sortOption
+ */
function changeUserDialogAvatarSorting(sortOption) {
const D = userDialog.value;
D.avatarSorting = sortOption;
sortUserDialogAvatars(D.avatars);
}
+ /**
+ *
+ */
function closeInviteDialog() {
clearInviteImageUpload();
}
+ /**
+ *
+ */
function getVRChatCredits() {
miscRequest.getVRChatCredits().then((args) => (vrchatCredit.value = args.json?.balance));
}
diff --git a/src/components/ui/input-group/InputGroupTextarea.vue b/src/components/ui/input-group/InputGroupTextarea.vue
index a2083668..a0b6f9a7 100644
--- a/src/components/ui/input-group/InputGroupTextarea.vue
+++ b/src/components/ui/input-group/InputGroupTextarea.vue
@@ -12,7 +12,7 @@
data-slot="input-group-control"
:class="
cn(
- 'flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 dark:bg-transparent',
+ 'flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 dark:bg-transparent break-all',
props.class
)
" />
diff --git a/src/localization/en.json b/src/localization/en.json
index dff4cc2a..155e9284 100644
--- a/src/localization/en.json
+++ b/src/localization/en.json
@@ -76,7 +76,7 @@
}
},
"side_panel": {
- "search_placeholder": "Search...",
+ "search_placeholder": "Quick Search...",
"search_no_results": "No results found",
"search_min_chars": "Type at least 2 characters",
"search_categories": "Search for...",
@@ -1935,7 +1935,7 @@
"export_bans": "Export Bans",
"import_bans": "Import Bans",
"import_bans_description": "Paste CSV or user IDs below. The userId column will be auto-detected from CSV headers, or all usr_ IDs will be extracted.",
- "import_bans_warning": "Do not import too many users at once. The API is rate-limited and may fail. You are responsible for any consequences.",
+ "import_bans_warning": "Do not import too many at once. This is a sensitive operation actively monitored and enforced by the official, use at your own risk!",
"import_bans_placeholder": "Paste CSV or user IDs here (one per line)\nusr_xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"import_bans_start": "Import & Ban",
"import_bans_clear_errors": "Clear Errors"
diff --git a/src/stores/ui.js b/src/stores/ui.js
index 02858c33..c7a68640 100644
--- a/src/stores/ui.js
+++ b/src/stores/ui.js
@@ -30,6 +30,7 @@ export const useUiStore = defineStore('Ui', () => {
const ctrlR = keys['Ctrl+R'];
const ctrlD = keys['Ctrl+D'];
+ const metaD = keys['Meta+D'];
const shift = keys['Shift'];
const ctrlShiftI = keys['Ctrl+Shift+I'];
const altShiftR = keys['Alt+Shift+R'];
@@ -51,6 +52,12 @@ export const useUiStore = defineStore('Ui', () => {
}
});
+ watch(metaD, (isPressed) => {
+ if (isPressed) {
+ directAccessPaste();
+ }
+ });
+
watch(shift, (isHeld) => {
shiftHeld.value = isHeld;
});
diff --git a/src/views/Sidebar/Sidebar.vue b/src/views/Sidebar/Sidebar.vue
index 15875217..9e6f9f50 100644
--- a/src/views/Sidebar/Sidebar.vue
+++ b/src/views/Sidebar/Sidebar.vue
@@ -4,7 +4,7 @@