mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-09-04 09:13:24 +00:00
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.
This commit is contained in:
@@ -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',
|
||||
])
|
||||
})
|
||||
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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('<sky-action-sheet')
|
||||
expect(source).toContain(
|
||||
'<sky-action-button bold @click="openProfileMediaApp(\'photos\')">',
|
||||
)
|
||||
expect(source).toContain(
|
||||
'<sky-action-button @click="openProfileMediaApp(\'camera\')">',
|
||||
)
|
||||
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('<sky-card', cardClass)
|
||||
const cardEnd = source.indexOf('</sky-card>', 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\)/,
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
SkyActionButton,
|
||||
SkyActionGroup,
|
||||
SkyActionSheet,
|
||||
SkyActionsLabel,
|
||||
SkyBadge,
|
||||
SkyBlock,
|
||||
SkyBlockTitle,
|
||||
@@ -124,6 +128,7 @@ const messageScroll = ref<HTMLElement | null>(null)
|
||||
const profileEditing = ref(false)
|
||||
const profileSettings = ref(false)
|
||||
const profileSaving = ref(false)
|
||||
const photoSourceOpened = ref(false)
|
||||
const discoverySaving = ref(false)
|
||||
const unmatchDialog = ref(false)
|
||||
const activeExploreMode = ref<ExploreMode>('all')
|
||||
@@ -297,14 +302,24 @@ function cloneDraft(): FlareProfileDraft {
|
||||
}
|
||||
}
|
||||
|
||||
function openPhotoGallery(): void {
|
||||
function openPhotoSourcePicker(): void {
|
||||
if (draftPhotos.value.length >= 6) return
|
||||
photoSourceOpened.value = true
|
||||
}
|
||||
|
||||
function closePhotoSourcePicker(): void {
|
||||
photoSourceOpened.value = false
|
||||
}
|
||||
|
||||
function openProfileMediaApp(app: 'camera' | 'photos'): void {
|
||||
const remaining = 6 - draftPhotos.value.length
|
||||
if (remaining < 1) return
|
||||
closePhotoSourcePicker()
|
||||
messageMedia.begin(
|
||||
'flare:profile-photos',
|
||||
'photo',
|
||||
flare.profile ? '/apps/flare?profileEdit=1' : '/apps/flare',
|
||||
remaining,
|
||||
app === 'photos' ? remaining : 1,
|
||||
{
|
||||
draft: cloneDraft(),
|
||||
editing: Boolean(flare.profile),
|
||||
@@ -312,7 +327,7 @@ function openPhotoGallery(): void {
|
||||
} satisfies FlareMediaContext,
|
||||
)
|
||||
void router.push({
|
||||
path: '/apps/photos',
|
||||
path: `/apps/${app}`,
|
||||
query: { mediaAttachment: 'photo' },
|
||||
})
|
||||
}
|
||||
@@ -382,7 +397,7 @@ function choiceLinkProps(field: FlareChoiceField): Record<string, unknown> {
|
||||
|
||||
async function openChoice(
|
||||
field: FlareChoiceField,
|
||||
trigger: HTMLElement,
|
||||
trigger: HTMLElement | null,
|
||||
): Promise<void> {
|
||||
activeChoiceTrigger = trigger
|
||||
activeChoiceField.value = field
|
||||
@@ -397,6 +412,11 @@ async function openChoice(
|
||||
?.focus({ preventScroll: true })
|
||||
}
|
||||
|
||||
async function openProfileGoalEditor(): Promise<void> {
|
||||
profileEditing.value = true
|
||||
await openChoice('lookingFor', null)
|
||||
}
|
||||
|
||||
function closeChoice(): void {
|
||||
const trigger = activeChoiceTrigger
|
||||
choiceOpened.value = false
|
||||
@@ -548,6 +568,7 @@ function openSettings(): void {
|
||||
}
|
||||
|
||||
function closeProfileScreen(): void {
|
||||
syncDraft()
|
||||
profileEditing.value = false
|
||||
profileSettings.value = false
|
||||
}
|
||||
@@ -924,10 +945,10 @@ onBeforeUnmount(() => {
|
||||
clear
|
||||
class="flare-photo-add"
|
||||
:class="{ 'is-empty': !draftPhotos.length }"
|
||||
@click="openPhotoGallery"
|
||||
@click="openPhotoSourcePicker"
|
||||
>
|
||||
<Images />
|
||||
<span>{{ phone.t('Apps.flare.choosePhotos') }}</span>
|
||||
<Plus />
|
||||
<span>{{ phone.t('Apps.flare.addPhotos') }}</span>
|
||||
</sky-button>
|
||||
</div>
|
||||
</sky-card>
|
||||
@@ -1617,7 +1638,16 @@ onBeforeUnmount(() => {
|
||||
{{ phone.t('Apps.easyShare.shareProfile') }}
|
||||
</button>
|
||||
</div>
|
||||
<sky-card :content-wrap="false" class="flare-profile-card">
|
||||
<sky-card
|
||||
component="button"
|
||||
type="button"
|
||||
:content-wrap="false"
|
||||
class="flare-profile-card"
|
||||
aria-controls="flare-choice-sheet"
|
||||
aria-haspopup="dialog"
|
||||
:aria-label="phone.t('Apps.flare.editRelationshipGoal')"
|
||||
@click="openProfileGoalEditor"
|
||||
>
|
||||
<span><Heart fill="currentColor" /></span>
|
||||
<div>
|
||||
<strong>{{
|
||||
@@ -1667,10 +1697,10 @@ onBeforeUnmount(() => {
|
||||
clear
|
||||
class="flare-photo-add"
|
||||
:class="{ 'is-empty': !draftPhotos.length }"
|
||||
@click="openPhotoGallery"
|
||||
@click="openPhotoSourcePicker"
|
||||
>
|
||||
<Images />
|
||||
<span>{{ phone.t('Apps.flare.choosePhotos') }}</span>
|
||||
<Plus />
|
||||
<span>{{ phone.t('Apps.flare.addPhotos') }}</span>
|
||||
</sky-button>
|
||||
</div>
|
||||
</sky-card>
|
||||
@@ -1856,8 +1886,42 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<sky-action-sheet
|
||||
:aria-label="phone.t('Apps.flare.addPhotos')"
|
||||
:opened="photoSourceOpened"
|
||||
@backdropclick="closePhotoSourcePicker"
|
||||
@escape="closePhotoSourcePicker"
|
||||
>
|
||||
<sky-action-group>
|
||||
<sky-actions-label>
|
||||
{{ phone.t('Apps.flare.addPhotos') }}
|
||||
</sky-actions-label>
|
||||
<sky-action-button bold @click="openProfileMediaApp('photos')">
|
||||
<span class="flare-photo-source-action">
|
||||
<Images :size="20" />
|
||||
{{ phone.t('Apps.flare.choosePhotos') }}
|
||||
</span>
|
||||
</sky-action-button>
|
||||
<sky-action-button @click="openProfileMediaApp('camera')">
|
||||
<span class="flare-photo-source-action">
|
||||
<Camera :size="20" />
|
||||
{{ phone.t('Apps.flare.takePhoto') }}
|
||||
</span>
|
||||
</sky-action-button>
|
||||
</sky-action-group>
|
||||
<sky-action-group>
|
||||
<sky-action-button @click="closePhotoSourcePicker">
|
||||
{{ phone.t('Common.cancel') }}
|
||||
</sky-action-button>
|
||||
</sky-action-group>
|
||||
</sky-action-sheet>
|
||||
|
||||
<div class="flare-choice-sheet">
|
||||
<sky-sheet :opened="choiceOpened" @backdropclick="closeChoice">
|
||||
<sky-sheet
|
||||
:opened="choiceOpened"
|
||||
@backdropclick="closeChoice"
|
||||
@escape="closeChoice"
|
||||
>
|
||||
<section
|
||||
id="flare-choice-sheet"
|
||||
ref="choiceSheetContent"
|
||||
@@ -2625,15 +2689,29 @@ onBeforeUnmount(() => {
|
||||
box-shadow: 0 7px 18px rgb(255 56 92 / 25%);
|
||||
}
|
||||
.flare-profile-card {
|
||||
box-sizing: border-box;
|
||||
width: calc(100% - 4px);
|
||||
min-height: 65px;
|
||||
margin: 26px 2px 0 !important;
|
||||
padding: 14px !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 11px;
|
||||
border: 0;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
border-radius: 15px !important;
|
||||
background: var(--flare-panel) !important;
|
||||
box-shadow: none !important;
|
||||
cursor: pointer;
|
||||
}
|
||||
.flare-profile-card:active {
|
||||
background: var(--sky-pressed) !important;
|
||||
}
|
||||
.flare-profile-card:focus-visible {
|
||||
outline: 2px solid var(--flare);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.flare-profile-card > span {
|
||||
width: 37px;
|
||||
@@ -2834,6 +2912,11 @@ onBeforeUnmount(() => {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
}
|
||||
.flare-photo-source-action {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--sky-space-2);
|
||||
}
|
||||
.flare-error {
|
||||
margin: 8px 22px 0;
|
||||
color: #ff3b30;
|
||||
|
||||
@@ -406,7 +406,7 @@ Locales["en"] = {
|
||||
flare = {
|
||||
name = "Flare", signInTitle = "Sign in to Flare", signInBody = "Flare profiles and matches are linked to your private Sky Cloud account.",
|
||||
createProfile = "Create your profile", welcome = "Find your spark", welcomeBody = "Build a profile and meet people across Los Santos.",
|
||||
photo = "Profile photo", profilePhotos = "Profile photos", profilePhotosBody = "Choose up to six photos from Photos. Your first photo is shown first.", addPhotos = "Add photos", choosePhotos = "Choose from Photos", primaryPhoto = "Main", removePhoto = "Remove photo {number}",
|
||||
photo = "Profile photo", profilePhotos = "Profile photos", profilePhotosBody = "Add up to six photos from Photos or Camera. Your first photo is shown first.", addPhotos = "Add photos", choosePhotos = "Choose from Photos", primaryPhoto = "Main", removePhoto = "Remove photo {number}",
|
||||
yourName = "Your name", namePlaceholder = "What should people call you?", age = "Age", bio = "About you", bioPlaceholder = "A short line that starts a conversation...",
|
||||
gender = "I am", showMe = "Show me", woman = "Woman", man = "Man", nonbinary = "Non-binary", women = "Women", men = "Men", nonbinaryPeople = "Non-binary people", everyone = "Everyone",
|
||||
minimumAge = "Minimum age", maximumAge = "Maximum age", start = "Start exploring", saveProfile = "Save profile", editProfileBody = "Keep your profile fresh and unmistakably you.",
|
||||
@@ -416,7 +416,7 @@ Locales["en"] = {
|
||||
exploreTitle = "Explore", exploreBody = "Find people who are into the same things.",
|
||||
exploreModes = { forYou = "For You", dateNight = "Date Night", freeTonight = "Free Tonight", longTerm = "Long-term thing", newFriends = "New Friends", weekend = "Weekend plans" },
|
||||
likesTitle = "Likes You", likesBody = "See who already likes you, then make your choice.", matched = "Matched", newMatches = "New Matches", messages = "Messages",
|
||||
settings = "Settings", editProfile = "Edit profile", profileGoalBody = "Shown on your discovery card.", like = "Like", pass = "Pass",
|
||||
settings = "Settings", editProfile = "Edit profile", editRelationshipGoal = "Edit relationship goal", profileGoalBody = "Shown on your discovery card.", like = "Like", pass = "Pass",
|
||||
relationshipGoal = "Relationship goal", interests = "Interests", interestsPlaceholder = "Music, travel, coffee", interestsHint = "Separate up to five interests with commas.",
|
||||
discoverySettings = "Discovery", discoveryPreferences = "Who you want to meet", showProfile = "Show me on Flare", showProfileBody = "Let new people discover your profile.",
|
||||
discoveryPrivacyNote = "Turning Discovery off hides you from new people. Your existing matches and chats stay available.",
|
||||
@@ -428,7 +428,7 @@ Locales["en"] = {
|
||||
messagePlaceholder = "Write a message", itsAMatch = "It's a spark!", matchBody = "You and {name} liked each other.", sayHello = "Say hello", keepSwiping = "Keep exploring", newMatchNotification = "You and {sender} found a spark!", newMessageNotification = "New message from {sender}",
|
||||
lookingFor = { longTerm = "Long-term connection", dates = "Good dates", friends = "New friends" },
|
||||
errors = {
|
||||
invalid_profile = "Check your name, age and profile text.", invalid_profile_photos = "Choose up to six photos from 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.", invalid_discovery = "That Discovery setting is invalid.", discovery_disabled = "Enable Discovery before swiping.",
|
||||
invalid_profile = "Check your name, age and profile text.", invalid_profile_photos = "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.", invalid_discovery = "That Discovery setting is invalid.", discovery_disabled = "Enable Discovery before swiping.",
|
||||
nothing_to_rewind = "There is no recent swipe to rewind.", cannot_rewind_match = "A swipe that created a match cannot be rewound.",
|
||||
match_not_found = "This match is no longer available.", invalid_message = "Write a message before sending.", invalid_attachment = "This attachment is unavailable.", gif_provider_unconfigured = "GIF search is not configured.", gif_provider_unauthorized = "The GIF provider key is invalid.", gif_provider_rate_limited = "GIF search is busy. Try again shortly.", gif_provider_failed = "GIFs are temporarily unavailable.", rate_limited = "Slow down for a moment and try again.", default = "Flare could not complete the request.",
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user