mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-09-04 17:23:25 +00:00
Merge branch 'dev' into feature/funk
This commit is contained in:
@@ -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()
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user