mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-19 14:53:50 +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')">
|
<DropdownMenuItem @click="onCommand('Request Invite')">
|
||||||
<Mail class="size-4" />
|
<Mail class="size-4" />
|
||||||
{{ t('dialog.user.actions.request_invite') }}
|
{{ 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>
|
||||||
<DropdownMenuItem @click="onCommand('Request Invite Message')">
|
<DropdownMenuItem @click="onCommand('Request Invite Message')">
|
||||||
<Mail class="size-4" />
|
<Mail class="size-4" />
|
||||||
{{ t('dialog.user.actions.request_invite_with_message') }}
|
{{ 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>
|
</DropdownMenuItem>
|
||||||
<template v-if="isGameRunning">
|
<template v-if="isGameRunning">
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
@@ -78,12 +84,18 @@
|
|||||||
@click="onCommand('Invite')">
|
@click="onCommand('Invite')">
|
||||||
<MessageSquare class="size-4" />
|
<MessageSquare class="size-4" />
|
||||||
{{ t('dialog.user.actions.invite') }}
|
{{ t('dialog.user.actions.invite') }}
|
||||||
|
<DropdownMenuShortcut v-if="isActionRecent(userDialog.id, 'Invite')">
|
||||||
|
<Clock class="size-3.5 text-muted-foreground" />
|
||||||
|
</DropdownMenuShortcut>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
:disabled="!checkCanInvite(lastLocation.location)"
|
:disabled="!checkCanInvite(lastLocation.location)"
|
||||||
@click="onCommand('Invite Message')">
|
@click="onCommand('Invite Message')">
|
||||||
<MessageSquare class="size-4" />
|
<MessageSquare class="size-4" />
|
||||||
{{ t('dialog.user.actions.invite_with_message') }}
|
{{ 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>
|
</DropdownMenuItem>
|
||||||
</template>
|
</template>
|
||||||
<DropdownMenuItem :disabled="!currentUser.isBoopingEnabled" @click="onCommand('Send Boop')">
|
<DropdownMenuItem :disabled="!currentUser.isBoopingEnabled" @click="onCommand('Send Boop')">
|
||||||
@@ -110,6 +122,9 @@
|
|||||||
<DropdownMenuItem v-else @click="onCommand('Send Friend Request')">
|
<DropdownMenuItem v-else @click="onCommand('Send Friend Request')">
|
||||||
<Plus class="size-4" />
|
<Plus class="size-4" />
|
||||||
{{ t('dialog.user.actions.send_friend_request') }}
|
{{ 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>
|
||||||
<DropdownMenuItem @click="onCommand('Invite To Group')">
|
<DropdownMenuItem @click="onCommand('Invite To Group')">
|
||||||
<MessageSquare class="size-4" />
|
<MessageSquare class="size-4" />
|
||||||
@@ -218,6 +233,7 @@
|
|||||||
import {
|
import {
|
||||||
Check,
|
Check,
|
||||||
CheckCircle,
|
CheckCircle,
|
||||||
|
Clock,
|
||||||
Flag,
|
Flag,
|
||||||
LineChart,
|
LineChart,
|
||||||
Mail,
|
Mail,
|
||||||
@@ -248,10 +264,12 @@
|
|||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuShortcut,
|
||||||
DropdownMenuTrigger
|
DropdownMenuTrigger
|
||||||
} from '../../ui/dropdown-menu';
|
} from '../../ui/dropdown-menu';
|
||||||
import { useGameStore, useLocationStore, useUserStore } from '../../../stores';
|
import { useGameStore, useLocationStore, useUserStore } from '../../../stores';
|
||||||
import { useInviteChecks } from '../../../composables/useInviteChecks';
|
import { useInviteChecks } from '../../../composables/useInviteChecks';
|
||||||
|
import { isActionRecent } from '../../../composables/useRecentActions';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
userDialogCommand: {
|
userDialogCommand: {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
} from '../../../api';
|
} from '../../../api';
|
||||||
import { copyToClipboard, parseLocation } from '../../../shared/utils';
|
import { copyToClipboard, parseLocation } from '../../../shared/utils';
|
||||||
import { database } from '../../../services/database';
|
import { database } from '../../../services/database';
|
||||||
|
import { recordRecentAction } from '../../../composables/useRecentActions';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Composable for UserDialog command dispatch.
|
* Composable for UserDialog command dispatch.
|
||||||
@@ -254,6 +255,7 @@ export function useUserDialogCommands(
|
|||||||
)
|
)
|
||||||
.then((args) => {
|
.then((args) => {
|
||||||
toast('Request invite sent');
|
toast('Request invite sent');
|
||||||
|
recordRecentAction(D().id, 'Request Invite');
|
||||||
return args;
|
return args;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -272,6 +274,7 @@ export function useUserDialogCommands(
|
|||||||
},
|
},
|
||||||
D().id
|
D().id
|
||||||
);
|
);
|
||||||
|
recordRecentAction(D().id, 'Invite Message');
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
'Request Invite Message': () => {
|
'Request Invite Message': () => {
|
||||||
@@ -281,6 +284,7 @@ export function useUserDialogCommands(
|
|||||||
},
|
},
|
||||||
D().id
|
D().id
|
||||||
);
|
);
|
||||||
|
recordRecentAction(D().id, 'Request Invite Message');
|
||||||
},
|
},
|
||||||
Invite: () => {
|
Invite: () => {
|
||||||
let currentLocation = lastLocation.value.location;
|
let currentLocation = lastLocation.value.location;
|
||||||
@@ -304,6 +308,7 @@ export function useUserDialogCommands(
|
|||||||
)
|
)
|
||||||
.then((_args) => {
|
.then((_args) => {
|
||||||
toast(t('message.invite.sent'));
|
toast(t('message.invite.sent'));
|
||||||
|
recordRecentAction(D().id, 'Invite');
|
||||||
return _args;
|
return _args;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -463,6 +468,7 @@ export function useUserDialogCommands(
|
|||||||
userId
|
userId
|
||||||
});
|
});
|
||||||
handleSendFriendRequest(args);
|
handleSendFriendRequest(args);
|
||||||
|
recordRecentAction(userId, 'Send Friend Request');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'Moderation Unblock': {
|
'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": {
|
"user_dialog": {
|
||||||
"header": "User Dialog",
|
"header": "User Dialog",
|
||||||
"vrchat_notes": "VRChat Notes",
|
"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": {
|
"user_colors": {
|
||||||
"header": "User Colors",
|
"header": "User Colors",
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ export const useGeneralSettingsStore = defineStore('GeneralSettings', () => {
|
|||||||
const autoStateChangeGroups = ref([]);
|
const autoStateChangeGroups = ref([]);
|
||||||
const autoAcceptInviteRequests = ref('Off');
|
const autoAcceptInviteRequests = ref('Off');
|
||||||
const autoAcceptInviteGroups = ref([]);
|
const autoAcceptInviteGroups = ref([]);
|
||||||
|
const recentActionCooldownEnabled = ref(false);
|
||||||
|
const recentActionCooldownMinutes = ref(60);
|
||||||
|
|
||||||
async function initGeneralSettings() {
|
async function initGeneralSettings() {
|
||||||
const [
|
const [
|
||||||
@@ -68,7 +70,9 @@ export const useGeneralSettingsStore = defineStore('GeneralSettings', () => {
|
|||||||
autoStateChangeCompanyDescConfig,
|
autoStateChangeCompanyDescConfig,
|
||||||
autoStateChangeGroupsStrConfig,
|
autoStateChangeGroupsStrConfig,
|
||||||
autoAcceptInviteRequestsConfig,
|
autoAcceptInviteRequestsConfig,
|
||||||
autoAcceptInviteGroupsStrConfig
|
autoAcceptInviteGroupsStrConfig,
|
||||||
|
recentActionCooldownEnabledConfig,
|
||||||
|
recentActionCooldownMinutesConfig
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
configRepository.getBool('VRCX_StartAtWindowsStartup', false),
|
configRepository.getBool('VRCX_StartAtWindowsStartup', false),
|
||||||
VRCXStorage.Get('VRCX_StartAsMinimizedState'),
|
VRCXStorage.Get('VRCX_StartAsMinimizedState'),
|
||||||
@@ -108,7 +112,9 @@ export const useGeneralSettingsStore = defineStore('GeneralSettings', () => {
|
|||||||
configRepository.getString('VRCX_autoStateChangeCompanyDesc', ''),
|
configRepository.getString('VRCX_autoStateChangeCompanyDesc', ''),
|
||||||
configRepository.getString('VRCX_autoStateChangeGroups', '[]'),
|
configRepository.getString('VRCX_autoStateChangeGroups', '[]'),
|
||||||
configRepository.getString('VRCX_autoAcceptInviteRequests', 'Off'),
|
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;
|
isStartAtWindowsStartup.value = isStartAtWindowsStartupConfig;
|
||||||
@@ -159,6 +165,8 @@ export const useGeneralSettingsStore = defineStore('GeneralSettings', () => {
|
|||||||
autoAcceptInviteGroups.value = JSON.parse(
|
autoAcceptInviteGroups.value = JSON.parse(
|
||||||
autoAcceptInviteGroupsStrConfig
|
autoAcceptInviteGroupsStrConfig
|
||||||
);
|
);
|
||||||
|
recentActionCooldownEnabled.value = recentActionCooldownEnabledConfig;
|
||||||
|
recentActionCooldownMinutes.value = recentActionCooldownMinutesConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
initGeneralSettings();
|
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 {
|
return {
|
||||||
isStartAtWindowsStartup,
|
isStartAtWindowsStartup,
|
||||||
isStartAsMinimizedState,
|
isStartAsMinimizedState,
|
||||||
@@ -435,6 +466,8 @@ export const useGeneralSettingsStore = defineStore('GeneralSettings', () => {
|
|||||||
autoStateChangeGroups,
|
autoStateChangeGroups,
|
||||||
autoAcceptInviteRequests,
|
autoAcceptInviteRequests,
|
||||||
autoAcceptInviteGroups,
|
autoAcceptInviteGroups,
|
||||||
|
recentActionCooldownEnabled,
|
||||||
|
recentActionCooldownMinutes,
|
||||||
|
|
||||||
setIsStartAtWindowsStartup,
|
setIsStartAtWindowsStartup,
|
||||||
setIsStartAsMinimizedState,
|
setIsStartAsMinimizedState,
|
||||||
@@ -459,6 +492,8 @@ export const useGeneralSettingsStore = defineStore('GeneralSettings', () => {
|
|||||||
setAutoStateChangeGroups,
|
setAutoStateChangeGroups,
|
||||||
setAutoAcceptInviteRequests,
|
setAutoAcceptInviteRequests,
|
||||||
setAutoAcceptInviteGroups,
|
setAutoAcceptInviteGroups,
|
||||||
promptProxySettings
|
promptProxySettings,
|
||||||
|
setRecentActionCooldownEnabled,
|
||||||
|
setRecentActionCooldownMinutes
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -264,6 +264,31 @@
|
|||||||
<SettingsItem :label="t('view.settings.appearance.user_dialog.vrcx_memos')">
|
<SettingsItem :label="t('view.settings.appearance.user_dialog.vrcx_memos')">
|
||||||
<Switch :model-value="!hideUserMemos" @update:modelValue="setHideUserMemos" />
|
<Switch :model-value="!hideUserMemos" @update:modelValue="setHideUserMemos" />
|
||||||
</SettingsItem>
|
</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>
|
||||||
|
|
||||||
<SettingsGroup :title="t('view.settings.appearance.friend_log.header')">
|
<SettingsGroup :title="t('view.settings.appearance.friend_log.header')">
|
||||||
@@ -335,7 +360,7 @@
|
|||||||
import { Popover, PopoverAnchor, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
import { Popover, PopoverAnchor, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||||
import { computed, onBeforeUnmount, ref, watch } from 'vue';
|
import { computed, onBeforeUnmount, ref, watch } from 'vue';
|
||||||
import { CheckIcon, ChevronDown } from 'lucide-vue-next';
|
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 { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
||||||
import { Switch } from '@/components/ui/switch';
|
import { Switch } from '@/components/ui/switch';
|
||||||
import { getLanguageName, languageCodes } from '@/localization';
|
import { getLanguageName, languageCodes } from '@/localization';
|
||||||
@@ -355,6 +380,7 @@
|
|||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
const appearanceSettingsStore = useAppearanceSettingsStore();
|
const appearanceSettingsStore = useAppearanceSettingsStore();
|
||||||
|
const generalSettingsStore = useGeneralSettingsStore();
|
||||||
const { saveOpenVROption, updateVRConfigVars } = useVrStore();
|
const { saveOpenVROption, updateVRConfigVars } = useVrStore();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -406,6 +432,16 @@
|
|||||||
setAppCjkFontPack
|
setAppCjkFontPack
|
||||||
} = appearanceSettingsStore;
|
} = appearanceSettingsStore;
|
||||||
|
|
||||||
|
const {
|
||||||
|
recentActionCooldownEnabled,
|
||||||
|
recentActionCooldownMinutes
|
||||||
|
} = storeToRefs(generalSettingsStore);
|
||||||
|
|
||||||
|
const {
|
||||||
|
setRecentActionCooldownEnabled,
|
||||||
|
setRecentActionCooldownMinutes
|
||||||
|
} = generalSettingsStore;
|
||||||
|
|
||||||
const trustColorEntries = [
|
const trustColorEntries = [
|
||||||
{
|
{
|
||||||
key: 'untrusted',
|
key: 'untrusted',
|
||||||
|
|||||||
Reference in New Issue
Block a user