mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-09-04 17:23:25 +00:00
ENH - modernize FlipTok and Messages
Replace the mixed legacy Konsta presentation and fixed light surfaces with the shared Sky UI system. Add FlipTok comment replies and reactions, editable profile media, follower lists, external sound metadata, and the required schema migration.
This commit is contained in:
@@ -16,6 +16,8 @@ vi.mock('@/utils/nui', () => ({
|
||||
|
||||
const profile: FlipTokProfile = {
|
||||
account_type: 'person',
|
||||
avatar_media_id: null,
|
||||
avatar_url: null,
|
||||
bio: '',
|
||||
display_name: 'Nova',
|
||||
followers: 1,
|
||||
@@ -29,6 +31,7 @@ const profile: FlipTokProfile = {
|
||||
}
|
||||
|
||||
const video: FlipTokVideo = {
|
||||
avatar_url: null,
|
||||
caption: 'Los Santos',
|
||||
comment_count: 1,
|
||||
comments_enabled: true,
|
||||
@@ -44,9 +47,11 @@ const video: FlipTokVideo = {
|
||||
location: '',
|
||||
cover_time_ms: 0,
|
||||
music_artist: '',
|
||||
music_source: '',
|
||||
music_title: '',
|
||||
music_track: '',
|
||||
music_url: '',
|
||||
music_video_id: '',
|
||||
music_volume: 0,
|
||||
original_volume: 100,
|
||||
profile_id: 7,
|
||||
@@ -59,16 +64,22 @@ const video: FlipTokVideo = {
|
||||
}
|
||||
|
||||
const comment: FlipTokComment = {
|
||||
avatar_url: null,
|
||||
body: 'Nice',
|
||||
created_at: 1,
|
||||
display_name: 'Nova',
|
||||
handle: 'nova',
|
||||
id: 'comment-1',
|
||||
is_liked: false,
|
||||
like_count: 0,
|
||||
parent_id: null,
|
||||
profile_id: 7,
|
||||
reply_to_handle: null,
|
||||
verified: false,
|
||||
}
|
||||
|
||||
const activity: FlipTokActivity = {
|
||||
avatar_url: null,
|
||||
created_at: 1,
|
||||
display_name: 'Nova',
|
||||
handle: 'nova',
|
||||
@@ -191,4 +202,35 @@ describe('FlipTok verification updates', () => {
|
||||
expect(await store.loadVideo(video.id)).toEqual(video)
|
||||
expect(nuiCall).toHaveBeenCalledWith('fliptok:video', { id: video.id })
|
||||
})
|
||||
|
||||
it('likes comments optimistically and restores them on failure', async () => {
|
||||
vi.mocked(nuiCall).mockResolvedValue({ success: false })
|
||||
const store = useFlipTokStore()
|
||||
const visibleComment = { ...comment }
|
||||
|
||||
await store.reactComment(visibleComment)
|
||||
|
||||
expect(nuiCall).toHaveBeenCalledWith('fliptok:comment-react', {
|
||||
active: true,
|
||||
id: visibleComment.id,
|
||||
})
|
||||
expect(visibleComment.is_liked).toBe(false)
|
||||
expect(visibleComment.like_count).toBe(0)
|
||||
})
|
||||
|
||||
it('loads the selected follower list', async () => {
|
||||
const follower = { ...profile, id: 8, is_owner: false }
|
||||
vi.mocked(nuiCall).mockResolvedValue({
|
||||
success: true,
|
||||
data: [follower],
|
||||
})
|
||||
const store = useFlipTokStore()
|
||||
|
||||
expect(await store.loadConnections(profile.id, 'followers')).toBe(true)
|
||||
expect(store.connections).toEqual([follower])
|
||||
expect(nuiCall).toHaveBeenCalledWith('fliptok:connections', {
|
||||
mode: 'followers',
|
||||
profileId: profile.id,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -17,6 +17,7 @@ export const useFlipTokStore = defineStore('fliptok', {
|
||||
activities: [] as FlipTokActivity[],
|
||||
authenticated: false,
|
||||
comments: [] as FlipTokComment[],
|
||||
connections: [] as FlipTokProfile[],
|
||||
feed: [] as FlipTokVideo[],
|
||||
isAdmin: false,
|
||||
loading: false,
|
||||
@@ -211,8 +212,35 @@ export const useFlipTokStore = defineStore('fliptok', {
|
||||
})
|
||||
this.comments = response.success && response.data ? response.data : []
|
||||
},
|
||||
async comment(id: string, body: string): Promise<NuiResponse> {
|
||||
return nuiCall('fliptok:comment', { body, id })
|
||||
async comment(
|
||||
id: string,
|
||||
body: string,
|
||||
parentId?: string,
|
||||
): Promise<NuiResponse> {
|
||||
return nuiCall('fliptok:comment', { body, id, parentId })
|
||||
},
|
||||
async reactComment(comment: FlipTokComment): Promise<void> {
|
||||
const active = !comment.is_liked
|
||||
comment.is_liked = active
|
||||
comment.like_count += active ? 1 : -1
|
||||
const response = await nuiCall('fliptok:comment-react', {
|
||||
active,
|
||||
id: comment.id,
|
||||
})
|
||||
if (response.success) return
|
||||
comment.is_liked = !active
|
||||
comment.like_count += active ? -1 : 1
|
||||
},
|
||||
async loadConnections(
|
||||
profileId: number,
|
||||
mode: 'followers' | 'following',
|
||||
): Promise<boolean> {
|
||||
const response = await nuiCall<FlipTokProfile[]>('fliptok:connections', {
|
||||
mode,
|
||||
profileId,
|
||||
})
|
||||
this.connections = response.success && response.data ? response.data : []
|
||||
return response.success
|
||||
},
|
||||
async loadActivities(): Promise<void> {
|
||||
const response = await nuiCall<FlipTokActivity[]>('fliptok:activities')
|
||||
|
||||
@@ -8,7 +8,7 @@ import type {
|
||||
} from '@/types/music'
|
||||
import { nuiCall } from '@/utils/nui'
|
||||
|
||||
type YouTubePlayer = {
|
||||
export type YouTubePlayer = {
|
||||
destroy: () => void
|
||||
getCurrentTime: () => number
|
||||
getDuration: () => number
|
||||
@@ -19,7 +19,7 @@ type YouTubePlayer = {
|
||||
setVolume: (volume: number) => void
|
||||
}
|
||||
|
||||
type YouTubeApi = {
|
||||
export type YouTubeApi = {
|
||||
Player: new (
|
||||
target: HTMLElement,
|
||||
options: {
|
||||
@@ -199,7 +199,7 @@ function bindAudioEvents(): void {
|
||||
})
|
||||
}
|
||||
|
||||
function loadYouTubeApi(): Promise<YouTubeApi> {
|
||||
export function loadYouTubeApi(): Promise<YouTubeApi> {
|
||||
if (window.YT?.Player) return Promise.resolve(window.YT)
|
||||
if (youtubeApiPromise) return youtubeApiPromise
|
||||
youtubeApiPromise = new Promise<YouTubeApi>((resolve, reject) => {
|
||||
|
||||
@@ -1344,18 +1344,24 @@ const defaultLocales: LocaleTree = {
|
||||
create: 'Create',
|
||||
activity: 'Activity',
|
||||
profile: 'Profile',
|
||||
navigation: 'FlipTok navigation',
|
||||
emptyFeed: 'No videos yet',
|
||||
emptyFeedBody: 'Follow creators or post the first FlipTok.',
|
||||
searchPlaceholder: 'Search creators and videos',
|
||||
clearSearch: 'Clear search',
|
||||
noActivity: 'No activity yet',
|
||||
followers: 'Followers',
|
||||
videos: 'Videos',
|
||||
emptyBio: 'No bio yet.',
|
||||
editProfile: 'Edit profile',
|
||||
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 Gallery',
|
||||
changeVideo: 'Change',
|
||||
caption: 'Caption',
|
||||
captionPlaceholder: 'Write a caption...',
|
||||
location: 'Add location',
|
||||
whoCanWatch: 'Who can watch',
|
||||
@@ -1374,6 +1380,10 @@ const defaultLocales: LocaleTree = {
|
||||
comments: 'Comments',
|
||||
noComments: 'No comments yet',
|
||||
addComment: 'Add comment...',
|
||||
reply: 'Reply',
|
||||
replyPlaceholder: 'Write a reply...',
|
||||
replyingTo: 'Replying to {handle}',
|
||||
cancelReply: 'Cancel reply',
|
||||
report: 'Report video',
|
||||
reportReason: 'Reason',
|
||||
reportDetails: 'Additional details (optional)',
|
||||
@@ -1393,6 +1403,19 @@ const defaultLocales: LocaleTree = {
|
||||
chooseSound: 'Choose music',
|
||||
originalOnly: 'Original sound only',
|
||||
noMusic: 'No music tracks are configured.',
|
||||
customSound: 'Custom sound',
|
||||
customSoundLink: 'YouTube or audio link',
|
||||
customSoundHint:
|
||||
'Paste a YouTube link or a direct HTTPS audio-file link.',
|
||||
customSoundPlaceholder:
|
||||
'https://youtu.be/... or https://example.com/song.mp3',
|
||||
customSoundFormats:
|
||||
'YouTube, YouTube Music, Shorts, MP3, M4A, AAC, OGG, OPUS, WAV, or WEBM',
|
||||
useCustomSound: 'Use sound',
|
||||
loadingSound: 'Loading sound',
|
||||
invalidCustomSoundLink:
|
||||
'Use a YouTube link or a direct public HTTPS audio-file link.',
|
||||
customSoundLoadFailed: 'This sound could not be loaded.',
|
||||
trimAndCover: 'Trim & cover',
|
||||
trimStart: 'Start',
|
||||
trimEnd: 'End',
|
||||
@@ -1410,6 +1433,12 @@ const defaultLocales: LocaleTree = {
|
||||
username: 'Username',
|
||||
bio: 'Bio',
|
||||
accountType: 'Account type',
|
||||
profilePhoto: 'Profile photo',
|
||||
changePhoto: 'Change photo',
|
||||
chooseFromGallery: 'Gallery',
|
||||
takePhoto: 'Camera',
|
||||
removePhoto: 'Remove photo',
|
||||
noConnections: 'No profiles to show',
|
||||
authTitle: 'Your FlipTok account',
|
||||
login: 'Sign In',
|
||||
register: 'Register',
|
||||
@@ -1456,10 +1485,13 @@ const defaultLocales: LocaleTree = {
|
||||
},
|
||||
errors: {
|
||||
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_comment: 'Enter a valid comment.',
|
||||
comments_disabled: 'Comments are disabled.',
|
||||
invalid_profile: 'Check your profile details.',
|
||||
invalid_profile_image: 'Choose a valid photo from this phone.',
|
||||
invalid_handle: 'Use 3–24 letters, numbers, dots, or underscores.',
|
||||
invalid_display_name: 'Enter a display name.',
|
||||
invalid_password: 'Password must be 8–72 characters.',
|
||||
@@ -1633,6 +1665,9 @@ const defaultLocales: LocaleTree = {
|
||||
officialContact: 'Official company contact',
|
||||
messagingUnavailable: 'This company contact does not accept messages.',
|
||||
filterUnread: 'Show Unread Messages',
|
||||
filterLabel: 'Conversation Filter',
|
||||
allMessages: 'All',
|
||||
unreadMessages: 'Unread',
|
||||
smsLabel: 'Text Message · SMS',
|
||||
photo: 'Photo',
|
||||
gif: 'GIF',
|
||||
|
||||
Reference in New Issue
Block a user