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(); });