FIX - stabilize FlipTok media interactions

Complete the FlipTok UI, store, mock API, and server audit. Add reliable photo-slide publishing, comments, privacy checks, action error handling, deletion, localization, and profile interaction states.

Replace the photo carousel scroll path with a GPU-accelerated drag and swipe track that supports rapid consecutive gestures, and cover the corrected behavior with focused contract, store, and mock smoke tests.
This commit is contained in:
Leon.Schmidt
2026-08-16 23:32:46 +02:00
parent 0d351f9743
commit 102af69e48
9 changed files with 1380 additions and 167 deletions
+26
View File
@@ -233,4 +233,30 @@ describe('FlipTok verification updates', () => {
profileId: profile.id,
})
})
it('does not show a follow state when the server rejects it', async () => {
vi.mocked(nuiCall).mockResolvedValue({ success: false })
const store = useFlipTokStore()
const creatorVideo = { ...video, is_owner: false, profile_id: 8 }
store.feed = [creatorVideo]
expect(await store.follow(creatorVideo)).toBe(false)
expect(creatorVideo.is_following).toBe(false)
})
it('removes an owned video from every local surface after deletion', async () => {
vi.mocked(nuiCall).mockResolvedValue({ success: true })
const store = useFlipTokStore()
store.profile = { ...profile, video_count: 1 }
store.feed = [{ ...video }]
store.searchResults = [{ ...video }]
store.profileVideos = [{ ...video }]
expect(await store.deleteVideo(video.id)).toBe(true)
expect(nuiCall).toHaveBeenCalledWith('fliptok:delete', { id: video.id })
expect(store.feed).toEqual([])
expect(store.searchResults).toEqual([])
expect(store.profileVideos).toEqual([])
expect(store.profile.video_count).toBe(0)
})
})
+27 -11
View File
@@ -137,26 +137,27 @@ export const useFlipTokStore = defineStore('fliptok', {
if (kind === 'like') video.like_count += next ? -1 : 1
}
},
async follow(video: FlipTokVideo): Promise<void> {
async follow(video: FlipTokVideo): Promise<boolean> {
const next = !video.is_following
const response = await nuiCall('fliptok:follow', {
active: next,
profileId: video.profile_id,
})
if (response.success)
this.feed
.filter((item) => item.profile_id === video.profile_id)
.forEach((item) => {
item.is_following = next
})
if (!response.success) return false
this.feed
.filter((item) => item.profile_id === video.profile_id)
.forEach((item) => {
item.is_following = next
})
return true
},
async followProfile(profile: FlipTokProfile): Promise<void> {
async followProfile(profile: FlipTokProfile): Promise<boolean> {
const next = !profile.is_following
const response = await nuiCall('fliptok:follow', {
active: next,
profileId: profile.id,
})
if (!response.success) return
if (!response.success) return false
profile.is_following = next
profile.followers += next ? 1 : -1
this.feed
@@ -164,6 +165,7 @@ export const useFlipTokStore = defineStore('fliptok', {
.forEach((item) => {
item.is_following = next
})
return true
},
async loadProfile(query: {
handle?: string
@@ -201,11 +203,12 @@ export const useFlipTokStore = defineStore('fliptok', {
if (this.viewedProfile?.id === profileId) this.viewedProfile = null
return true
},
async loadComments(id: string): Promise<void> {
async loadComments(id: string): Promise<boolean> {
const response = await nuiCall<FlipTokComment[]>('fliptok:comments', {
id,
})
this.comments = response.success && response.data ? response.data : []
return response.success
},
async comment(
id: string,
@@ -237,10 +240,23 @@ export const useFlipTokStore = defineStore('fliptok', {
this.connections = response.success && response.data ? response.data : []
return response.success
},
async loadActivities(): Promise<void> {
async loadActivities(): Promise<boolean> {
const response = await nuiCall<FlipTokActivity[]>('fliptok:activities')
this.activities = response.success && response.data ? response.data : []
if (response.success) await nuiCall('fliptok:mark-activities')
return response.success
},
async deleteVideo(id: string): Promise<boolean> {
const response = await nuiCall('fliptok:delete', { id })
if (!response.success) return false
this.feed = this.feed.filter((video) => video.id !== id)
this.searchResults = this.searchResults.filter((video) => video.id !== id)
this.profileVideos = this.profileVideos.filter((video) => video.id !== id)
if (this.profile && this.profile.video_count > 0)
this.profile.video_count -= 1
if (this.viewedProfile?.is_owner && this.viewedProfile.video_count > 0)
this.viewedProfile.video_count -= 1
return true
},
},
})
+30 -6
View File
@@ -1467,18 +1467,28 @@ const defaultLocales: LocaleTree = {
emptyFeedBody: 'Follow creators or post the first FlipTok.',
searchPlaceholder: 'Search creators and videos',
clearSearch: 'Clear search',
trendLosSantos: '# Los Santos',
trendRoleplay: '# Roleplay',
trendTrending: '# Trending',
noActivity: 'No activity yet',
followers: 'Followers',
videos: 'Videos',
emptyBio: 'No bio yet.',
editProfile: 'Edit profile',
more: 'More',
newVideo: 'New FlipTok',
createTitle: 'Create your next FlipTok',
createBody:
'Choose a clip, set the cover and sound, then decide who can watch it.',
chooseVideo: 'Choose a video',
chooseVideoHint: 'Select one from Photos',
changeVideo: 'Change',
createBody: 'Record a clip or turn photos into a swipeable FlipTok.',
chooseMedia: 'Choose media',
chooseMediaHint:
'Record a video, choose a clip, or select up to 10 photos.',
recordVideo: 'Record video',
camera: 'Camera',
chooseVideo: 'Choose video',
photoSlideshow: 'Photos as FlipTok',
changeMedia: 'Change',
previousPhoto: 'Previous photo',
nextPhoto: 'Next photo',
caption: 'Caption',
captionPlaceholder: 'Write a caption...',
location: 'Add location',
@@ -1492,6 +1502,11 @@ const defaultLocales: LocaleTree = {
post: 'Post',
draftSaved: 'Draft saved.',
published: 'Your FlipTok is live.',
videoDeleted: 'Video removed.',
deleteVideoTitle: 'Remove this video?',
deleteVideoBody:
'The video disappears from FlipTok and cannot be restored.',
deletingVideo: 'Removing...',
linkCopied: 'Video link copied.',
reported: 'Report submitted.',
blocked: 'Creator blocked.',
@@ -1499,6 +1514,8 @@ const defaultLocales: LocaleTree = {
noComments: 'No comments yet',
addComment: 'Add comment...',
reply: 'Reply',
showReplies: 'Show {count} replies',
hideReplies: 'Hide {count} replies',
replyPlaceholder: 'Write a reply...',
replyingTo: 'Replying to {handle}',
cancelReply: 'Cancel reply',
@@ -1506,6 +1523,8 @@ const defaultLocales: LocaleTree = {
reportReason: 'Reason',
reportDetails: 'Additional details (optional)',
submitReport: 'Submit report',
reportDiscordNote:
'This report is sent directly to the moderation team through Discord.',
reportReasons: {
spam: 'Spam or misleading',
harassment: 'Harassment or bullying',
@@ -1547,6 +1566,7 @@ const defaultLocales: LocaleTree = {
dismissReport: 'Dismiss',
cancel: 'Cancel',
done: 'Done',
savingProfile: 'Saving...',
displayName: 'Name',
username: 'Username',
bio: 'Bio',
@@ -1605,7 +1625,7 @@ const defaultLocales: LocaleTree = {
invalid_video: 'Check the video details.',
invalid_music_url:
'Use a valid YouTube or direct HTTPS audio-file link.',
invalid_media: 'Choose a video from this phone.',
invalid_media: 'Choose valid media from this phone.',
invalid_comment: 'Enter a valid comment.',
comments_disabled: 'Comments are disabled.',
invalid_profile: 'Check your profile details.',
@@ -1618,11 +1638,15 @@ const defaultLocales: LocaleTree = {
'This Sky Cloud account already owns a registered FlipTok profile.',
handle_taken: 'This username is already taken.',
video_not_found: 'This video is unavailable.',
profile_not_found: 'This profile is unavailable.',
rate_limited: 'Too many actions. Try again shortly.',
not_authenticated: 'Sign in to Sky Cloud first.',
blocked: 'This account is blocked.',
not_authorized: 'You do not have moderation access.',
report_not_found: 'This report is no longer open.',
report_unavailable: 'Discord reporting is not configured yet.',
request_failed:
'Your FlipTok request could not be completed. Try again.',
default: 'FlipTok could not complete the request.',
},
},