mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-13 20:03:51 +02:00
feat: Add recent action indicators for invites and friend requests (#809)
This commit is contained in:
@@ -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: {
|
||||
|
||||
@@ -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': {
|
||||
|
||||
57
src/composables/useRecentActions.js
Normal file
57
src/composables/useRecentActions.js
Normal 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 = {};
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user