From d75f2c38b48de1a00018e8a3ebdef9b84d42c6df Mon Sep 17 00:00:00 2001 From: "Leon.Schmidt" Date: Sun, 16 Aug 2026 20:11:28 +0200 Subject: [PATCH] ENH - refine Picstagram posting and profiles Improve multi-image post previews, profile navigation, privacy controls, and follow-request activity handling. Align post actions, comments, and profile filters with the intended Picstagram interaction model and cover the behavior with focused tests. --- frontend/src/stores/phone.ts | 5 + frontend/src/stores/picstagram.test.ts | 15 + frontend/src/stores/picstagram.ts | 2 + .../views/apps/PicstagramApp.contract.test.ts | 24 +- frontend/src/views/apps/PicstagramApp.vue | 396 +++++++++++++----- sky_phone/config/locales/en.lua | 6 +- 6 files changed, 348 insertions(+), 100 deletions(-) diff --git a/frontend/src/stores/phone.ts b/frontend/src/stores/phone.ts index 172867b..eaf7c3f 100644 --- a/frontend/src/stores/phone.ts +++ b/frontend/src/stores/phone.ts @@ -1071,6 +1071,8 @@ const defaultLocales: LocaleTree = { chooseStoryHint: 'Select one photo or video', selectedPhotos: '{count} selected', changePhotos: 'Change selection', + previousPhoto: 'Previous photo', + nextPhoto: 'Next photo', caption: 'Caption', captionPlaceholder: 'Write a caption...', storyTextPlaceholder: 'Add a short story text...', @@ -1113,10 +1115,13 @@ const defaultLocales: LocaleTree = { shareProfile: 'Share profile', share: 'Share', followRequests: 'Follow requests', + requestAccepted: 'Follow request accepted.', + requestDeclined: 'Follow request declined.', noActivity: 'No activity yet', markRead: 'Mark as read', publicProfile: 'Public profile', privateProfileSetting: 'Private profile', + profileVisibility: 'Profile visibility', privacyHint: 'Only accepted followers can see your posts and stories.', bio: 'Bio', avatar: 'Profile photo', diff --git a/frontend/src/stores/picstagram.test.ts b/frontend/src/stores/picstagram.test.ts index 51d1f6b..e97f829 100644 --- a/frontend/src/stores/picstagram.test.ts +++ b/frontend/src/stores/picstagram.test.ts @@ -152,6 +152,21 @@ describe('Picstagram store', () => { expect(store.explore[0].like_count).toBe(2) }) + it('accepts a private follow request from activity and updates followers', async () => { + vi.mocked(nuiCall).mockResolvedValue({ success: true }) + const store = usePicstagramStore() + store.profile = { ...profile, followers: 4 } + store.activities = [{ ...activity, kind: 'follow_request' }] + + expect(await store.respondFollow(profile.id, true)).toBe(true) + expect(store.activities).toEqual([]) + expect(store.profile.followers).toBe(5) + expect(nuiCall).toHaveBeenCalledWith('picstagram:respond-follow', { + accept: true, + profileId: profile.id, + }) + }) + it('removes a blocked profile from all local surfaces', async () => { vi.mocked(nuiCall).mockResolvedValue({ success: true }) const store = usePicstagramStore() diff --git a/frontend/src/stores/picstagram.ts b/frontend/src/stores/picstagram.ts index d77cf7c..60af151 100644 --- a/frontend/src/stores/picstagram.ts +++ b/frontend/src/stores/picstagram.ts @@ -271,6 +271,8 @@ export const usePicstagramStore = defineStore('picstagram', { activity.kind !== 'follow_request' || activity.profile_id !== profileId, ) + if (response.success && accept && this.profile) + this.profile.followers += 1 return response.success }, async loadComments(id: string): Promise { diff --git a/frontend/src/views/apps/PicstagramApp.contract.test.ts b/frontend/src/views/apps/PicstagramApp.contract.test.ts index 8a29dba..0cdc0c7 100644 --- a/frontend/src/views/apps/PicstagramApp.contract.test.ts +++ b/frontend/src/views/apps/PicstagramApp.contract.test.ts @@ -28,16 +28,36 @@ describe('Picstagram app layout', () => { it('supports a visible multi-image compose workflow', () => { expect(source).toContain("mediaType === 'photo' ? 5 : 1") expect(source).toContain('class="ps-selection-slide"') - expect(source).toContain('@click="removeComposeMedia(media.id)"') + expect(source).toContain('@click="moveComposePreview(-1)"') + expect(source).toContain('@click="moveComposePreview(1)"') + expect(source).toContain('composePreviewIndex + 1') expect(source).toContain('@click="changeComposeSelection"') + expect(source).toContain('class="ps-publish-link"') }) - it('uses owner actions and all, video, and tagged profile filters', () => { + it('uses Instagram-like owner actions and profile filters', () => { expect(source).toContain('class="ps-profile-owner-actions"') expect(source).toContain('@click="editProfile"') expect(source).toContain('@click="shareCurrentProfile"') expect(source).toContain("profileSection === 'all'") expect(source).toContain("profileSection === 'videos'") expect(source).toContain("profileSection === 'tagged'") + expect(source).toContain('class="ps-profile-filters"') + expect(source).toMatch( + /\.ps-profile-owner-actions\s*\{[^}]*grid-template-columns:\s*repeat\(2,/s, + ) + }) + + it('keeps create out of navigation and exposes privacy requests', () => { + expect(source).toContain(':item-count="4"') + expect(source).not.toContain('ps-create-tab') + expect(source).toContain('profileDraft.private = false') + expect(source).toContain('profileDraft.private = true') + expect(source).toContain( + 'respondToFollowRequest(activity.profile_id, true)', + ) + expect(source).toContain( + 'respondToFollowRequest(activity.profile_id, false)', + ) }) }) diff --git a/frontend/src/views/apps/PicstagramApp.vue b/frontend/src/views/apps/PicstagramApp.vue index a1fb99d..8f253b8 100644 --- a/frontend/src/views/apps/PicstagramApp.vue +++ b/frontend/src/views/apps/PicstagramApp.vue @@ -97,6 +97,7 @@ const authConfirmPassword = ref('') const authSubmitting = ref(false) const composeKind = ref('post') const selectedMedia = ref([]) +const composePreviewIndex = ref(0) const caption = ref('') const location = ref('') const storyText = ref('') @@ -152,7 +153,10 @@ const unreadCount = computed( () => store.activities.filter((activity) => !activity.read_at).length, ) const tabIndex = computed(() => - ['home', 'explore', 'create', 'activity', 'profile'].indexOf(tab.value), + ['home', 'explore', 'activity', 'profile'].indexOf(tab.value), +) +const currentComposeMedia = computed( + () => selectedMedia.value[composePreviewIndex.value] ?? null, ) const taggedProfilePosts = computed(() => { const handle = currentProfile.value?.handle.trim().toLowerCase() @@ -447,6 +451,7 @@ function openAvatarMedia(source: MediaSource): void { function resetComposer(): void { selectedMedia.value = [] + composePreviewIndex.value = 0 caption.value = '' location.value = '' storyText.value = '' @@ -455,6 +460,20 @@ function resetComposer(): void { function removeComposeMedia(id: number): void { selectedMedia.value = selectedMedia.value.filter((media) => media.id !== id) + composePreviewIndex.value = Math.min( + composePreviewIndex.value, + Math.max(0, selectedMedia.value.length - 1), + ) +} + +function moveComposePreview(direction: 1 | -1): void { + composePreviewIndex.value = Math.max( + 0, + Math.min( + selectedMedia.value.length - 1, + composePreviewIndex.value + direction, + ), + ) } function changeComposeSelection(): void { @@ -762,6 +781,17 @@ function activityProfile(activity: PicstagramActivity): void { void openProfile(activity.profile_id) } +async function respondToFollowRequest( + profileId: string, + accept: boolean, +): Promise { + if (!(await store.respondFollow(profileId, accept))) { + notify(errorMessage()) + return + } + notify(t(accept ? 'requestAccepted' : 'requestDeclined')) +} + watch(search, (value) => { if (searchTimer !== null) window.clearTimeout(searchTimer) searchTimer = window.setTimeout(() => { @@ -791,6 +821,7 @@ onMounted(async () => { }>('picstagram:compose') if (composeSelection?.media.length) { selectedMedia.value = composeSelection.media + composePreviewIndex.value = 0 composeKind.value = composeSelection.context?.kind ?? 'post' caption.value = composeSelection.context?.caption ?? '' location.value = composeSelection.context?.location ?? '' @@ -1347,8 +1378,24 @@ onBeforeUnmount(() => { @@ -1513,6 +1572,9 @@ onBeforeUnmount(() => { v-for="activity in store.activities" :key="activity.id" class="ps-activity-row" + :class="{ + 'ps-activity-row--request': activity.kind === 'follow_request', + }" @click="activityProfile(activity)" > { {{ t('accept') }}{{ t('decline') }} @@ -1584,11 +1646,11 @@ onBeforeUnmount(() => {