diff --git a/src/components/dialogs/UserDialog/SocialStatusDialog.vue b/src/components/dialogs/UserDialog/SocialStatusDialog.vue
index be5aaffd..d2a70864 100644
--- a/src/components/dialogs/UserDialog/SocialStatusDialog.vue
+++ b/src/components/dialogs/UserDialog/SocialStatusDialog.vue
@@ -59,7 +59,32 @@
+
+
+ {{ t('dialog.social_status.presets') }}
+
+
+
+
+ {{ preset.statusDescription || getStatusLabel(preset.status) }}
+
+
+
+
+
+
@@ -77,7 +102,7 @@
DropdownMenuItem,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu';
- import { Check, History } from 'lucide-vue-next';
+ import { Bookmark, Check, History, X } from 'lucide-vue-next';
import { InputGroupButton, InputGroupField } from '@/components/ui/input-group';
import { Button } from '@/components/ui/button';
import { computed } from 'vue';
@@ -87,6 +112,7 @@
import { useUserStore } from '../../../stores';
import { userRequest } from '../../../api';
+ import { useStatusPresets } from './composables/useStatusPresets';
const { t } = useI18n();
const { currentUser } = storeToRefs(useUserStore());
@@ -104,6 +130,35 @@
const historyItems = computed(() => props.socialStatusHistoryTable?.data ?? []);
+ const { presets, addPreset, removePreset, getStatusClass, MAX_PRESETS } = useStatusPresets();
+
+ function getStatusLabel(status) {
+ const option = statusOptions.value.find((o) => o.value === status);
+ return option?.label || status;
+ }
+
+ function applyPreset(preset) {
+ const D = props.socialStatusDialog;
+ D.status = preset.status;
+ D.statusDescription = preset.statusDescription;
+ }
+
+ async function handleSavePreset() {
+ const D = props.socialStatusDialog;
+ const result = await addPreset(D.status, D.statusDescription);
+ if (result === 'ok') {
+ toast.success(t('dialog.social_status.preset_saved'));
+ } else if (result === 'exists') {
+ toast.info(t('dialog.social_status.preset_exists'));
+ } else if (result === 'limit') {
+ toast.warning(t('dialog.social_status.preset_limit', { max: MAX_PRESETS }));
+ }
+ }
+
+ async function handleDeletePreset(index) {
+ await removePreset(index);
+ }
+
const statusOptions = computed(() => {
const options = [
{
diff --git a/src/components/dialogs/UserDialog/composables/useStatusPresets.js b/src/components/dialogs/UserDialog/composables/useStatusPresets.js
new file mode 100644
index 00000000..c1baae92
--- /dev/null
+++ b/src/components/dialogs/UserDialog/composables/useStatusPresets.js
@@ -0,0 +1,56 @@
+import { ref } from 'vue';
+import configRepository from '../../../../services/config';
+
+const STORAGE_KEY = 'VRCX_statusPresets';
+const MAX_PRESETS = 10;
+
+const STATUS_CLASS_MAP = {
+ 'join me': 'joinme',
+ active: 'online',
+ 'ask me': 'askme',
+ busy: 'busy',
+ offline: 'offline'
+};
+
+const presets = ref([]);
+let loadPromise = null;
+
+export function useStatusPresets() {
+ async function loadPresets() {
+ try {
+ presets.value = await configRepository.getArray(STORAGE_KEY, []);
+ } catch {
+ presets.value = [];
+ }
+ }
+
+ async function addPreset(status, statusDescription) {
+ if (presets.value.length >= MAX_PRESETS) {
+ return 'limit';
+ }
+ const exists = presets.value.some(
+ (p) => p.status === status && p.statusDescription === statusDescription
+ );
+ if (exists) {
+ return 'exists';
+ }
+ presets.value.push({ status, statusDescription });
+ await configRepository.setArray(STORAGE_KEY, presets.value);
+ return 'ok';
+ }
+
+ async function removePreset(index) {
+ presets.value.splice(index, 1);
+ await configRepository.setArray(STORAGE_KEY, presets.value);
+ }
+
+ function getStatusClass(status) {
+ return STATUS_CLASS_MAP[status] || 'online';
+ }
+
+ if (!loadPromise) {
+ loadPromise = loadPresets();
+ }
+
+ return { presets, loadPresets, addPreset, removePreset, getStatusClass, MAX_PRESETS };
+}
diff --git a/src/localization/en.json b/src/localization/en.json
index 1706b22a..0c3a927d 100644
--- a/src/localization/en.json
+++ b/src/localization/en.json
@@ -1544,7 +1544,12 @@
"header": "Social Status",
"history": "History",
"status_placeholder": "Status",
- "update": "Update"
+ "update": "Update",
+ "presets": "Presets",
+ "save_preset": "Save as Preset",
+ "preset_saved": "Preset saved",
+ "preset_exists": "Preset already exists",
+ "preset_limit": "Maximum {max} presets"
},
"language": {
"header": "Language",
diff --git a/src/views/Sidebar/components/FriendsSidebar.vue b/src/views/Sidebar/components/FriendsSidebar.vue
index ed6ed6de..87efba44 100644
--- a/src/views/Sidebar/components/FriendsSidebar.vue
+++ b/src/views/Sidebar/components/FriendsSidebar.vue
@@ -108,6 +108,21 @@
+
+
+ {{ t('dialog.social_status.presets') }}
+
+
+
+
+ {{ getPresetDisplayText(preset) }}
+
+
+
@@ -218,6 +233,7 @@
import FriendItem from './FriendItem.vue';
import Location from '../../../components/Location.vue';
import configRepository from '../../../services/config';
+ import { useStatusPresets } from '../../../components/dialogs/UserDialog/composables/useStatusPresets';
import '@/styles/status-icon.css';
import { showUserDialog } from '../../../coordinators/userCoordinator';
@@ -252,6 +268,7 @@
const { currentUser } = storeToRefs(useUserStore());
const { checkCanInvite, checkCanInviteSelf } = useInviteChecks();
const { userImage, userStatusClass } = useUserDisplay();
+ const { presets: statusPresets, getStatusClass: presetStatusClass } = useStatusPresets();
const isFriendsGroupMe = ref(true);
const isVIPFriends = ref(true);
@@ -720,6 +737,23 @@
});
}
+ function getPresetDisplayText(preset) {
+ if (preset.statusDescription) return preset.statusDescription;
+ const option = statusOptions.value.find((o) => o.value === preset.status);
+ return option?.label || preset.status;
+ }
+
+ function applyStatusPreset(preset) {
+ userRequest
+ .saveCurrentUser({
+ status: preset.status,
+ statusDescription: preset.statusDescription
+ })
+ .then(() => {
+ toast.success('Status updated');
+ });
+ }
+
const canInviteToMyLocation = computed(() => checkCanInvite(lastLocation.value.location));
/**
diff --git a/src/views/Sidebar/components/__tests__/FriendsSidebar.test.js b/src/views/Sidebar/components/__tests__/FriendsSidebar.test.js
index 09ddd59a..5e0b390c 100644
--- a/src/views/Sidebar/components/__tests__/FriendsSidebar.test.js
+++ b/src/views/Sidebar/components/__tests__/FriendsSidebar.test.js
@@ -60,7 +60,9 @@ const mocks = vi.hoisted(() => ({
},
configRepository: {
getBool: vi.fn(),
- setBool: vi.fn()
+ setBool: vi.fn(),
+ getArray: vi.fn(),
+ setArray: vi.fn()
},
notificationRequest: {
sendRequestInvite: vi.fn().mockResolvedValue({}),
@@ -251,6 +253,8 @@ describe('FriendsSidebar.vue', () => {
(_key, defaultValue) => Promise.resolve(defaultValue ?? false)
);
mocks.configRepository.setBool.mockResolvedValue(undefined);
+ mocks.configRepository.getArray.mockResolvedValue([]);
+ mocks.configRepository.setArray.mockResolvedValue(undefined);
vi.clearAllMocks();
});