Merge branch 'dev' into feature/funk

This commit is contained in:
Alec Schitzkat
2026-08-09 02:24:01 +02:00
29 changed files with 4538 additions and 66 deletions
+133
View File
@@ -0,0 +1,133 @@
import { createPinia, setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { useFlipTokStore } from '@/stores/fliptok'
import type {
FlipTokActivity,
FlipTokComment,
FlipTokProfile,
FlipTokVideo,
} from '@/types/fliptok'
import { nuiCall } from '@/utils/nui'
vi.mock('@/utils/nui', () => ({
nuiCall: vi.fn(),
}))
const profile: FlipTokProfile = {
account_type: 'person',
bio: '',
display_name: 'Nova',
followers: 1,
following: 2,
handle: 'nova',
id: 7,
is_following: false,
is_owner: true,
verified: false,
video_count: 1,
}
const video: FlipTokVideo = {
caption: 'Los Santos',
comment_count: 1,
comments_enabled: true,
created_at: 1,
display_name: 'Nova',
handle: 'nova',
id: 'video-1',
is_following: false,
is_liked: false,
is_owner: true,
is_saved: false,
like_count: 2,
location: '',
cover_time_ms: 0,
music_artist: '',
music_title: '',
music_track: '',
music_url: '',
music_volume: 0,
original_volume: 100,
profile_id: 7,
share_count: 0,
trim_end_ms: null,
trim_start_ms: 0,
url: 'https://example.com/video.webm',
verified: false,
view_count: 3,
}
const comment: FlipTokComment = {
body: 'Nice',
created_at: 1,
display_name: 'Nova',
handle: 'nova',
id: 'comment-1',
profile_id: 7,
verified: false,
}
const activity: FlipTokActivity = {
created_at: 1,
display_name: 'Nova',
handle: 'nova',
id: 'activity-1',
kind: 'follow',
profile_id: 7,
read_at: null,
verified: false,
video_id: null,
}
describe('FlipTok verification updates', () => {
beforeEach(() => {
setActivePinia(createPinia())
vi.mocked(nuiCall).mockReset()
})
it('updates the badge everywhere the profile is already visible', () => {
const store = useFlipTokStore()
store.profile = { ...profile }
store.feed = [{ ...video }]
store.searchResults = [{ ...video }]
store.comments = [{ ...comment }]
store.activities = [{ ...activity }]
store.applyVerification(7, true)
expect(store.profile.verified).toBe(true)
expect(store.feed[0].verified).toBe(true)
expect(store.searchResults[0].verified).toBe(true)
expect(store.comments[0].verified).toBe(true)
expect(store.activities[0].verified).toBe(true)
})
it('does not alter another profile', () => {
const store = useFlipTokStore()
store.feed = [{ ...video }]
store.applyVerification(99, true)
expect(store.feed[0].verified).toBe(false)
})
it('removes a blocked creator from every visible surface', async () => {
vi.mocked(nuiCall).mockResolvedValue({ success: true })
const store = useFlipTokStore()
store.feed = [{ ...video }]
store.searchResults = [{ ...video }]
store.profileVideos = [{ ...video }]
store.comments = [{ ...comment }]
store.activities = [{ ...activity }]
store.viewedProfile = { ...profile }
expect(await store.blockProfile(7)).toBe(true)
expect(store.feed).toEqual([])
expect(store.searchResults).toEqual([])
expect(store.profileVideos).toEqual([])
expect(store.comments).toEqual([])
expect(store.activities).toEqual([])
expect(store.viewedProfile).toBeNull()
})
})
+208
View File
@@ -0,0 +1,208 @@
import { defineStore } from 'pinia'
import type {
FlipTokActivity,
FlipTokComment,
FlipTokMusicTrack,
FlipTokPage,
FlipTokProfile,
FlipTokProfilePage,
FlipTokReport,
FlipTokVideo,
} from '@/types/fliptok'
import { nuiCall, type NuiResponse } from '@/utils/nui'
export const useFlipTokStore = defineStore('fliptok', {
state: () => ({
activities: [] as FlipTokActivity[],
comments: [] as FlipTokComment[],
feed: [] as FlipTokVideo[],
isAdmin: false,
loading: false,
musicTracks: [] as FlipTokMusicTrack[],
mode: 'for-you' as 'for-you' | 'following',
profile: null as FlipTokProfile | null,
profileVideos: [] as FlipTokVideo[],
reports: [] as FlipTokReport[],
searchResults: [] as FlipTokVideo[],
viewedProfile: null as FlipTokProfile | null,
}),
actions: {
applyVerification(profileId: number, verified: boolean): void {
if (this.profile?.id === profileId) this.profile.verified = verified
if (this.viewedProfile?.id === profileId)
this.viewedProfile.verified = verified
this.feed
.filter((video) => video.profile_id === profileId)
.forEach((video) => {
video.verified = verified
})
this.searchResults
.filter((video) => video.profile_id === profileId)
.forEach((video) => {
video.verified = verified
})
this.profileVideos
.filter((video) => video.profile_id === profileId)
.forEach((video) => {
video.verified = verified
})
this.comments
.filter((comment) => comment.profile_id === profileId)
.forEach((comment) => {
comment.verified = verified
})
this.activities
.filter((activity) => activity.profile_id === profileId)
.forEach((activity) => {
activity.verified = verified
})
},
async bootstrap(): Promise<boolean> {
this.loading = true
const response = await nuiCall<{
feed: FlipTokPage
isAdmin: boolean
musicTracks: FlipTokMusicTrack[]
profile: FlipTokProfile
}>('fliptok:bootstrap')
this.loading = false
if (!response.success || !response.data) return false
this.profile = response.data.profile
this.feed = response.data.feed.items
this.isAdmin = response.data.isAdmin === true
this.musicTracks = response.data.musicTracks ?? []
return true
},
async loadFeed(mode?: 'for-you' | 'following'): Promise<boolean> {
mode ??= this.mode
this.mode = mode
this.loading = true
const response = await nuiCall<FlipTokPage>('fliptok:feed', {
mode,
offset: 0,
})
this.loading = false
if (!response.success || !response.data) return false
this.feed = response.data.items
return true
},
async discover(search: string): Promise<FlipTokVideo[]> {
const response = await nuiCall<FlipTokVideo[]>('fliptok:discover', {
search,
})
this.searchResults =
response.success && response.data ? response.data : []
return this.searchResults
},
async react(video: FlipTokVideo, kind: 'like' | 'save'): Promise<void> {
const key = kind === 'like' ? 'is_liked' : 'is_saved'
const next = !video[key]
video[key] = next
if (kind === 'like') video.like_count += next ? 1 : -1
const response = await nuiCall('fliptok:react', {
active: next,
id: video.id,
kind,
})
if (!response.success) {
video[key] = !next
if (kind === 'like') video.like_count += next ? -1 : 1
}
},
async follow(video: FlipTokVideo): Promise<void> {
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
})
},
async followProfile(profile: FlipTokProfile): Promise<void> {
const next = !profile.is_following
const response = await nuiCall('fliptok:follow', {
active: next,
profileId: profile.id,
})
if (!response.success) return
profile.is_following = next
profile.followers += next ? 1 : -1
this.feed
.filter((item) => item.profile_id === profile.id)
.forEach((item) => {
item.is_following = next
})
},
async loadProfile(query: {
handle?: string
profileId?: number
}): Promise<boolean> {
const response = await nuiCall<FlipTokProfilePage>(
'fliptok:profile',
query,
)
if (!response.success || !response.data) return false
this.viewedProfile = response.data.profile
this.profileVideos = response.data.videos
return true
},
showOwnProfile(): void {
this.viewedProfile = null
this.profileVideos = this.feed.filter((item) => item.is_owner)
},
async blockProfile(profileId: number): Promise<boolean> {
const response = await nuiCall('fliptok:block', { profileId })
if (!response.success) return false
this.feed = this.feed.filter((video) => video.profile_id !== profileId)
this.searchResults = this.searchResults.filter(
(video) => video.profile_id !== profileId,
)
this.comments = this.comments.filter(
(comment) => comment.profile_id !== profileId,
)
this.activities = this.activities.filter(
(activity) => activity.profile_id !== profileId,
)
this.profileVideos = this.profileVideos.filter(
(video) => video.profile_id !== profileId,
)
if (this.viewedProfile?.id === profileId) this.viewedProfile = null
return true
},
async loadComments(id: string): Promise<void> {
const response = await nuiCall<FlipTokComment[]>('fliptok:comments', {
id,
})
this.comments = response.success && response.data ? response.data : []
},
async comment(id: string, body: string): Promise<NuiResponse> {
return nuiCall('fliptok:comment', { body, id })
},
async loadActivities(): Promise<void> {
const response = await nuiCall<FlipTokActivity[]>('fliptok:activities')
this.activities = response.success && response.data ? response.data : []
if (response.success) await nuiCall('fliptok:mark-activities')
},
async loadReports(): Promise<boolean> {
const response = await nuiCall<FlipTokReport[]>('fliptok:admin-reports')
this.reports = response.success && response.data ? response.data : []
return response.success
},
async resolveReport(
id: string,
action: 'dismiss' | 'remove',
): Promise<boolean> {
const response = await nuiCall('fliptok:admin-resolve-report', {
action,
id,
})
if (response.success) await this.loadReports()
return response.success
},
},
})
+120
View File
@@ -40,6 +40,121 @@ const namespaceQueues = new Map<string, Promise<void>>()
const defaultLocales: LocaleTree = {
Apps: {
fliptok: {
name: 'FlipTok',
loading: 'Loading FlipTok',
following: 'Following',
forYou: 'For You',
verified: 'Verified account',
originalSound: 'original sound',
save: 'Save',
home: 'Home',
discover: 'Discover',
create: 'Create',
activity: 'Activity',
profile: 'Profile',
emptyFeed: 'No videos yet',
emptyFeedBody: 'Follow creators or post the first FlipTok.',
searchPlaceholder: 'Search creators and videos',
noActivity: 'No activity yet',
followers: 'Followers',
videos: 'Videos',
emptyBio: 'No bio yet.',
editProfile: 'Edit profile',
newVideo: 'New FlipTok',
chooseVideo: 'Choose a video',
chooseVideoHint: 'Select one from Gallery',
changeVideo: 'Change',
captionPlaceholder: 'Write a caption...',
location: 'Add location',
whoCanWatch: 'Who can watch',
public: 'Everyone',
followersOnly: 'Followers',
private: 'Only me',
allowComments: 'Allow comments',
saveDraft: 'Drafts',
publishing: 'Posting...',
post: 'Post',
draftSaved: 'Draft saved.',
published: 'Your FlipTok is live.',
linkCopied: 'Video link copied.',
reported: 'Report submitted.',
blocked: 'Creator blocked.',
comments: 'Comments',
noComments: 'No comments yet',
addComment: 'Add comment...',
report: 'Report video',
reportReason: 'Reason',
reportDetails: 'Additional details (optional)',
submitReport: 'Submit report',
reportReasons: {
spam: 'Spam or misleading',
harassment: 'Harassment or bullying',
dangerous: 'Dangerous activity',
illegal: 'Illegal content',
other: 'Something else',
},
block: 'Block creator',
follow: 'Follow',
unfollow: 'Following',
backToProfile: 'Back',
sounds: 'Sound',
chooseSound: 'Choose music',
originalOnly: 'Original sound only',
noMusic: 'No music tracks are configured.',
trimAndCover: 'Trim & cover',
trimStart: 'Start',
trimEnd: 'End',
coverFrame: 'Cover',
originalVolume: 'Original sound',
musicVolume: 'Music',
moderation: 'Moderation',
reports: 'Open reports',
noReports: 'No open reports',
removeVideo: 'Remove video',
dismissReport: 'Dismiss',
cancel: 'Cancel',
done: 'Done',
displayName: 'Name',
username: 'Username',
bio: 'Bio',
accountType: 'Account type',
accountTypes: {
person: 'Person',
business: 'Business',
organization: 'Organization',
media: 'Media',
event: 'Event',
},
activityKinds: {
like: 'liked your video',
comment: 'commented on your video',
follow: 'started following you',
verified: 'verification changed',
},
notifications: {
like: '{actor} liked your video.',
comment: '{actor} commented on your video.',
follow: '{actor} started following you.',
verified: 'Your FlipTok account is now verified.',
default: 'You have new FlipTok activity.',
},
errors: {
invalid_video: 'Check the video details.',
invalid_media: 'Choose a video from this phone.',
invalid_comment: 'Enter a valid comment.',
comments_disabled: 'Comments are disabled.',
invalid_profile: 'Check your profile details.',
handle_taken: 'This username is already taken.',
video_not_found: 'This video is unavailable.',
rate_limited: 'Too many actions. Try again shortly.',
not_authenticated: 'Sign in to iFruit first.',
blocked: 'This account is blocked.',
not_authorized: 'You do not have moderation access.',
report_not_found: 'This report is no longer open.',
default: 'FlipTok could not complete the request.',
},
},
darkchat: {
name: 'DarkChat',
newMessage: 'New DarkChat message from {sender}',
@@ -1070,6 +1185,8 @@ const defaultLocales: LocaleTree = {
portrait: 'Switch to portrait',
photo: 'Photo',
video: 'Video',
microphoneOn: 'Microphone on',
microphoneOff: 'Microphone muted',
focusHelp: 'Space for movement',
returnHelp: 'Space to return',
uploading: '{count} uploading',
@@ -1087,6 +1204,8 @@ const defaultLocales: LocaleTree = {
invalid_upload: 'The upload could not be verified.',
invalid_upload_token: 'The upload session is no longer valid.',
missing_config: 'Camera uploads are not configured.',
microphone_unavailable:
'Allow microphone access or mute the microphone before recording.',
not_found: 'The media item no longer exists.',
operation_in_progress:
'Another media operation is already in progress.',
@@ -1544,6 +1663,7 @@ const defaultLocales: LocaleTree = {
send: 'Send',
start: 'Start',
stop: 'Stop',
use: 'Use',
},
Notifications: { now: 'now' },
LockScreen: {