diff --git a/src/components/dialogs/UserDialog/UserActionDropdown.vue b/src/components/dialogs/UserDialog/UserActionDropdown.vue index 1a28af24..f26e5bbe 100644 --- a/src/components/dialogs/UserDialog/UserActionDropdown.vue +++ b/src/components/dialogs/UserDialog/UserActionDropdown.vue @@ -67,10 +67,16 @@ {{ t('dialog.user.actions.request_invite') }} + + + {{ t('dialog.user.actions.request_invite_with_message') }} + + + @@ -110,6 +122,9 @@ {{ t('dialog.user.actions.send_friend_request') }} + + + @@ -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: { diff --git a/src/components/dialogs/UserDialog/useUserDialogCommands.js b/src/components/dialogs/UserDialog/useUserDialogCommands.js index 89fb47b9..99aff712 100644 --- a/src/components/dialogs/UserDialog/useUserDialogCommands.js +++ b/src/components/dialogs/UserDialog/useUserDialogCommands.js @@ -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': { diff --git a/src/composables/useRecentActions.js b/src/composables/useRecentActions.js new file mode 100644 index 00000000..bac3b7c0 --- /dev/null +++ b/src/composables/useRecentActions.js @@ -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 = {}; +} diff --git a/src/localization/en.json b/src/localization/en.json index 0c3a927d..c900cbb3 100644 --- a/src/localization/en.json +++ b/src/localization/en.json @@ -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", diff --git a/src/stores/settings/general.js b/src/stores/settings/general.js index 59bce3d1..05500d13 100644 --- a/src/stores/settings/general.js +++ b/src/stores/settings/general.js @@ -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 }; }); diff --git a/src/views/Settings/components/Tabs/AppearanceTab.vue b/src/views/Settings/components/Tabs/AppearanceTab.vue index 8fcd1610..fc1e56f0 100644 --- a/src/views/Settings/components/Tabs/AppearanceTab.vue +++ b/src/views/Settings/components/Tabs/AppearanceTab.vue @@ -264,6 +264,31 @@ + + + + + + + + + + + + + + @@ -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',