feat: Add recent action indicators for invites and friend requests (#809)

This commit is contained in:
pa
2026-03-16 13:31:40 +09:00
parent 03bb1b5410
commit 9bf380f2fc
6 changed files with 160 additions and 5 deletions

View File

@@ -67,10 +67,16 @@
<DropdownMenuItem @click="onCommand('Request Invite')">
<Mail class="size-4" />
{{ t('dialog.user.actions.request_invite') }}
<DropdownMenuShortcut v-if="isActionRecent(userDialog.id, 'Request Invite')">
<Clock class="size-3.5 text-muted-foreground" />
</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem @click="onCommand('Request Invite Message')">
<Mail class="size-4" />
{{ t('dialog.user.actions.request_invite_with_message') }}
<DropdownMenuShortcut v-if="isActionRecent(userDialog.id, 'Request Invite Message')">
<Clock class="size-3.5 text-muted-foreground" />
</DropdownMenuShortcut>
</DropdownMenuItem>
<template v-if="isGameRunning">
<DropdownMenuItem
@@ -78,12 +84,18 @@
@click="onCommand('Invite')">
<MessageSquare class="size-4" />
{{ t('dialog.user.actions.invite') }}
<DropdownMenuShortcut v-if="isActionRecent(userDialog.id, 'Invite')">
<Clock class="size-3.5 text-muted-foreground" />
</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem
:disabled="!checkCanInvite(lastLocation.location)"
@click="onCommand('Invite Message')">
<MessageSquare class="size-4" />
{{ t('dialog.user.actions.invite_with_message') }}
<DropdownMenuShortcut v-if="isActionRecent(userDialog.id, 'Invite Message')">
<Clock class="size-3.5 text-muted-foreground" />
</DropdownMenuShortcut>
</DropdownMenuItem>
</template>
<DropdownMenuItem :disabled="!currentUser.isBoopingEnabled" @click="onCommand('Send Boop')">
@@ -110,6 +122,9 @@
<DropdownMenuItem v-else @click="onCommand('Send Friend Request')">
<Plus class="size-4" />
{{ t('dialog.user.actions.send_friend_request') }}
<DropdownMenuShortcut v-if="isActionRecent(userDialog.id, 'Send Friend Request')">
<Clock class="size-3.5 text-muted-foreground" />
</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem @click="onCommand('Invite To Group')">
<MessageSquare class="size-4" />
@@ -218,6 +233,7 @@
import {
Check,
CheckCircle,
Clock,
Flag,
LineChart,
Mail,
@@ -248,10 +264,12 @@
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger
} from '../../ui/dropdown-menu';
import { useGameStore, useLocationStore, useUserStore } from '../../../stores';
import { useInviteChecks } from '../../../composables/useInviteChecks';
import { isActionRecent } from '../../../composables/useRecentActions';
const props = defineProps({
userDialogCommand: {

View File

@@ -10,6 +10,7 @@ import {
} from '../../../api';
import { copyToClipboard, parseLocation } from '../../../shared/utils';
import { database } from '../../../services/database';
import { recordRecentAction } from '../../../composables/useRecentActions';
/**
* Composable for UserDialog command dispatch.
@@ -254,6 +255,7 @@ export function useUserDialogCommands(
)
.then((args) => {
toast('Request invite sent');
recordRecentAction(D().id, 'Request Invite');
return args;
});
},
@@ -272,6 +274,7 @@ export function useUserDialogCommands(
},
D().id
);
recordRecentAction(D().id, 'Invite Message');
});
},
'Request Invite Message': () => {
@@ -281,6 +284,7 @@ export function useUserDialogCommands(
},
D().id
);
recordRecentAction(D().id, 'Request Invite Message');
},
Invite: () => {
let currentLocation = lastLocation.value.location;
@@ -304,6 +308,7 @@ export function useUserDialogCommands(
)
.then((_args) => {
toast(t('message.invite.sent'));
recordRecentAction(D().id, 'Invite');
return _args;
});
});
@@ -463,6 +468,7 @@ export function useUserDialogCommands(
userId
});
handleSendFriendRequest(args);
recordRecentAction(userId, 'Send Friend Request');
}
},
'Moderation Unblock': {

View File

@@ -0,0 +1,57 @@
import { useLocalStorage } from '@vueuse/core';
import { useGeneralSettingsStore } from '../stores/settings/general';
/**
* Persisted Map tracking recent invite/request actions.
* Key: `${userId}:${actionType}`, Value: timestamp (ms).
* Stored in localStorage via @vueuse/core.
*/
const recentActions = useLocalStorage('VRCX_recentActions', {});
const TRACKED_ACTIONS = new Set([
'Send Friend Request',
'Request Invite',
'Invite',
'Request Invite Message',
'Invite Message'
]);
/**
* @param {string} userId
* @param {string} actionType
*/
export function recordRecentAction(userId, actionType) {
if (!TRACKED_ACTIONS.has(actionType)) {
return;
}
recentActions.value[`${userId}:${actionType}`] = Date.now();
}
/**
* @param {string} userId
* @param {string} actionType
* @returns {boolean}
*/
export function isActionRecent(userId, actionType) {
const generalSettings = useGeneralSettingsStore();
if (!generalSettings.recentActionCooldownEnabled) {
return false;
}
const key = `${userId}:${actionType}`;
const ts = recentActions.value[key];
if (!ts) {
return false;
}
const cooldownMs = generalSettings.recentActionCooldownMinutes * 60 * 1000;
if (Date.now() - ts < cooldownMs) {
return true;
}
// Expired, clean up
delete recentActions.value[key];
return false;
}
export function clearRecentActions() {
recentActions.value = {};
}

View File

@@ -798,7 +798,10 @@
"user_dialog": {
"header": "User Dialog",
"vrchat_notes": "VRChat Notes",
"vrcx_memos": "VRCX Memos"
"vrcx_memos": "VRCX Memos",
"recent_action_cooldown": "Recent Action Indicator",
"recent_action_cooldown_description": "Show a clock icon on invite/request actions that were recently performed",
"recent_action_cooldown_minutes": "Cooldown (minutes)"
},
"user_colors": {
"header": "User Colors",

View File

@@ -42,6 +42,8 @@ export const useGeneralSettingsStore = defineStore('GeneralSettings', () => {
const autoStateChangeGroups = ref([]);
const autoAcceptInviteRequests = ref('Off');
const autoAcceptInviteGroups = ref([]);
const recentActionCooldownEnabled = ref(false);
const recentActionCooldownMinutes = ref(60);
async function initGeneralSettings() {
const [
@@ -68,7 +70,9 @@ export const useGeneralSettingsStore = defineStore('GeneralSettings', () => {
autoStateChangeCompanyDescConfig,
autoStateChangeGroupsStrConfig,
autoAcceptInviteRequestsConfig,
autoAcceptInviteGroupsStrConfig
autoAcceptInviteGroupsStrConfig,
recentActionCooldownEnabledConfig,
recentActionCooldownMinutesConfig
] = await Promise.all([
configRepository.getBool('VRCX_StartAtWindowsStartup', false),
VRCXStorage.Get('VRCX_StartAsMinimizedState'),
@@ -108,7 +112,9 @@ export const useGeneralSettingsStore = defineStore('GeneralSettings', () => {
configRepository.getString('VRCX_autoStateChangeCompanyDesc', ''),
configRepository.getString('VRCX_autoStateChangeGroups', '[]'),
configRepository.getString('VRCX_autoAcceptInviteRequests', 'Off'),
configRepository.getString('VRCX_autoAcceptInviteGroups', '[]')
configRepository.getString('VRCX_autoAcceptInviteGroups', '[]'),
configRepository.getBool('VRCX_recentActionCooldownEnabled', false),
configRepository.getInt('VRCX_recentActionCooldownMinutes', 60)
]);
isStartAtWindowsStartup.value = isStartAtWindowsStartupConfig;
@@ -159,6 +165,8 @@ export const useGeneralSettingsStore = defineStore('GeneralSettings', () => {
autoAcceptInviteGroups.value = JSON.parse(
autoAcceptInviteGroupsStrConfig
);
recentActionCooldownEnabled.value = recentActionCooldownEnabledConfig;
recentActionCooldownMinutes.value = recentActionCooldownMinutesConfig;
}
initGeneralSettings();
@@ -411,6 +419,29 @@ export const useGeneralSettingsStore = defineStore('GeneralSettings', () => {
});
}
function setRecentActionCooldownEnabled() {
recentActionCooldownEnabled.value =
!recentActionCooldownEnabled.value;
configRepository.setBool(
'VRCX_recentActionCooldownEnabled',
recentActionCooldownEnabled.value
);
}
/**
* @param {number} value
*/
function setRecentActionCooldownMinutes(value) {
const parsed = parseInt(value, 10);
recentActionCooldownMinutes.value = Number.isNaN(parsed)
? 60
: Math.min(1440, Math.max(1, parsed));
configRepository.setInt(
'VRCX_recentActionCooldownMinutes',
recentActionCooldownMinutes.value
);
}
return {
isStartAtWindowsStartup,
isStartAsMinimizedState,
@@ -435,6 +466,8 @@ export const useGeneralSettingsStore = defineStore('GeneralSettings', () => {
autoStateChangeGroups,
autoAcceptInviteRequests,
autoAcceptInviteGroups,
recentActionCooldownEnabled,
recentActionCooldownMinutes,
setIsStartAtWindowsStartup,
setIsStartAsMinimizedState,
@@ -459,6 +492,8 @@ export const useGeneralSettingsStore = defineStore('GeneralSettings', () => {
setAutoStateChangeGroups,
setAutoAcceptInviteRequests,
setAutoAcceptInviteGroups,
promptProxySettings
promptProxySettings,
setRecentActionCooldownEnabled,
setRecentActionCooldownMinutes
};
});

View File

@@ -264,6 +264,31 @@
<SettingsItem :label="t('view.settings.appearance.user_dialog.vrcx_memos')">
<Switch :model-value="!hideUserMemos" @update:modelValue="setHideUserMemos" />
</SettingsItem>
<SettingsItem
:label="t('view.settings.appearance.user_dialog.recent_action_cooldown')"
:description="t('view.settings.appearance.user_dialog.recent_action_cooldown_description')">
<Switch :model-value="recentActionCooldownEnabled" @update:modelValue="setRecentActionCooldownEnabled" />
</SettingsItem>
<SettingsItem
v-if="recentActionCooldownEnabled"
:label="t('view.settings.appearance.user_dialog.recent_action_cooldown_minutes')">
<NumberField
:model-value="recentActionCooldownMinutes"
:min="1"
:max="1440"
:step="1"
:format-options="{ maximumFractionDigits: 0 }"
class="w-32"
@update:modelValue="setRecentActionCooldownMinutes">
<NumberFieldContent>
<NumberFieldDecrement />
<NumberFieldInput />
<NumberFieldIncrement />
</NumberFieldContent>
</NumberField>
</SettingsItem>
</SettingsGroup>
<SettingsGroup :title="t('view.settings.appearance.friend_log.header')">
@@ -335,7 +360,7 @@
import { Popover, PopoverAnchor, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { computed, onBeforeUnmount, ref, watch } from 'vue';
import { CheckIcon, ChevronDown } from 'lucide-vue-next';
import { useAppearanceSettingsStore, useFavoriteStore, useVrStore } from '@/stores';
import { useAppearanceSettingsStore, useFavoriteStore, useGeneralSettingsStore, useVrStore } from '@/stores';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import { Switch } from '@/components/ui/switch';
import { getLanguageName, languageCodes } from '@/localization';
@@ -355,6 +380,7 @@
const { t } = useI18n();
const appearanceSettingsStore = useAppearanceSettingsStore();
const generalSettingsStore = useGeneralSettingsStore();
const { saveOpenVROption, updateVRConfigVars } = useVrStore();
const {
@@ -406,6 +432,16 @@
setAppCjkFontPack
} = appearanceSettingsStore;
const {
recentActionCooldownEnabled,
recentActionCooldownMinutes
} = storeToRefs(generalSettingsStore);
const {
setRecentActionCooldownEnabled,
setRecentActionCooldownMinutes
} = generalSettingsStore;
const trustColorEntries = [
{
key: 'untrusted',