From 91bf310778b6c6a8fb988f58f23364e982924e72 Mon Sep 17 00:00:00 2001 From: Dominik Date: Sun, 16 Aug 2026 16:31:16 +0200 Subject: [PATCH] ENH - improve Flare profile editing Add a Sky action sheet for gallery and camera profile photos while preserving ordered multi-photo drafts. Make relationship goals directly editable, restore cancelled drafts, localize the new interactions, and cover the flows with focused contracts. --- frontend/src/stores/flare.test.ts | 17 ++- frontend/src/stores/phone.ts | 5 +- .../src/views/apps/FlareApp.contract.test.ts | 51 +++++++++ frontend/src/views/apps/FlareApp.vue | 107 ++++++++++++++++-- sky_phone/config/locales/en.lua | 6 +- 5 files changed, 164 insertions(+), 22 deletions(-) create mode 100644 frontend/src/views/apps/FlareApp.contract.test.ts diff --git a/frontend/src/stores/flare.test.ts b/frontend/src/stores/flare.test.ts index 1e4d12a..c56ccce 100644 --- a/frontend/src/stores/flare.test.ts +++ b/frontend/src/stores/flare.test.ts @@ -81,13 +81,17 @@ describe('flare store', () => { }) }) - it('sends only Gallery media ids when profile photos are saved', async () => { + it('preserves ordered profile media ids and urls when profile photos are saved', async () => { const updated = { ...bootstrap, profile: { ...bootstrap.profile!, - photoMediaIds: [42], - photoUrls: ['https://cdn.example.test/profile.jpg'], + photoMediaIds: [42, 17, 91], + photoUrls: [ + 'https://cdn.example.test/profile-primary.jpg', + 'https://cdn.example.test/profile-secondary.jpg', + 'https://cdn.example.test/profile-tertiary.jpg', + ], }, } const draft: FlareProfileDraft = { @@ -101,15 +105,18 @@ describe('flare store', () => { maxAge: bootstrap.profile!.maxAge, minAge: bootstrap.profile!.minAge, name: bootstrap.profile!.name, - photoMediaIds: [42], + photoMediaIds: [42, 17, 91], } mockNuiCall.mockResolvedValueOnce({ data: updated, success: true }) const flare = useFlareStore() expect(await flare.saveProfile(draft)).toBe(true) expect(mockNuiCall).toHaveBeenCalledWith('flare:save-profile', draft) + expect(flare.profile?.photoMediaIds).toEqual([42, 17, 91]) expect(flare.profile?.photoUrls).toEqual([ - 'https://cdn.example.test/profile.jpg', + 'https://cdn.example.test/profile-primary.jpg', + 'https://cdn.example.test/profile-secondary.jpg', + 'https://cdn.example.test/profile-tertiary.jpg', ]) }) diff --git a/frontend/src/stores/phone.ts b/frontend/src/stores/phone.ts index 90fff29..135de68 100644 --- a/frontend/src/stores/phone.ts +++ b/frontend/src/stores/phone.ts @@ -846,7 +846,7 @@ const defaultLocales: LocaleTree = { photo: 'Profile photo', profilePhotos: 'Profile photos', profilePhotosBody: - 'Choose up to six photos from Photos. Your first photo is shown first.', + 'Add up to six photos from Photos or Camera. Your first photo is shown first.', addPhotos: 'Add photos', choosePhotos: 'Choose from Photos', primaryPhoto: 'Main', @@ -904,6 +904,7 @@ const defaultLocales: LocaleTree = { messages: 'Messages', settings: 'Settings', editProfile: 'Edit profile', + editRelationshipGoal: 'Edit relationship goal', profileGoalBody: 'Shown on your discovery card.', relationshipGoal: 'Relationship goal', interests: 'Interests', @@ -967,7 +968,7 @@ const defaultLocales: LocaleTree = { errors: { invalid_profile: 'Check your name, age and profile text.', invalid_profile_photos: - 'Choose up to six photos from your own Photos library.', + 'Choose or take up to six photos saved in your own Photos library.', request_failed: 'Flare could not save those changes. Try again.', invalid_target: 'This profile is no longer available.', invalid_choice: 'That swipe could not be saved.', diff --git a/frontend/src/views/apps/FlareApp.contract.test.ts b/frontend/src/views/apps/FlareApp.contract.test.ts new file mode 100644 index 0000000..5f93ccb --- /dev/null +++ b/frontend/src/views/apps/FlareApp.contract.test.ts @@ -0,0 +1,51 @@ +import { readFileSync } from 'node:fs' + +import { describe, expect, it } from 'vitest' + +const source = readFileSync( + new URL('./FlareApp.vue', import.meta.url), + 'utf8', +) + +describe('FlareApp profile editing contract', () => { + it('offers Gallery and Camera as profile photo sources', () => { + const mediaAppStart = source.indexOf('function openProfileMediaApp') + const mediaAppEnd = source.indexOf('function removeDraftPhoto') + const mediaApp = source.slice(mediaAppStart, mediaAppEnd) + + expect(source.match(/@click="openPhotoSourcePicker"/g)).toHaveLength(2) + expect(source).toContain('', + ) + expect(source).toContain( + '', + ) + expect(mediaAppStart).toBeGreaterThan(-1) + expect(mediaAppEnd).toBeGreaterThan(mediaAppStart) + expect(mediaApp).toContain("app: 'camera' | 'photos'") + expect(mediaApp).toContain("'flare:profile-photos'") + expect(mediaApp).toContain("app === 'photos' ? remaining : 1") + expect(mediaApp).toContain('void router.push({') + expect(mediaApp).toContain("query: { mediaAttachment: 'photo' }") + }) + + it('opens the relationship goal editor from the profile summary card', () => { + const cardClass = source.indexOf('class="flare-profile-card"') + const cardStart = source.lastIndexOf('', cardClass) + const card = source.slice(cardStart, cardEnd) + + expect(cardClass).toBeGreaterThan(-1) + expect(cardStart).toBeGreaterThan(-1) + expect(cardEnd).toBeGreaterThan(cardStart) + expect(card).toContain('component="button"') + expect(card).toContain('aria-controls="flare-choice-sheet"') + expect(card).toContain('aria-haspopup="dialog"') + expect(card).toContain('@click="openProfileGoalEditor"') + expect(source).toContain('async function openProfileGoalEditor()') + expect(source).toMatch( + /openProfileGoalEditor\(\)[\s\S]*?profileEditing\.value = true[\s\S]*?openChoice\('lookingFor', null\)/, + ) + }) +}) diff --git a/frontend/src/views/apps/FlareApp.vue b/frontend/src/views/apps/FlareApp.vue index 032f467..b3e7e0b 100644 --- a/frontend/src/views/apps/FlareApp.vue +++ b/frontend/src/views/apps/FlareApp.vue @@ -1,5 +1,9 @@