ADD - implement Picstagram app

This commit is contained in:
Leon.Schmidt
2026-08-09 14:46:31 +02:00
parent e7eafb207f
commit 2840b2e3ea
19 changed files with 6341 additions and 5 deletions
+44
View File
@@ -33,6 +33,7 @@ import { useMessagesStore } from '@/stores/messages'
import { useDarkChatStore } from '@/stores/darkchat'
import { useFlareStore } from '@/stores/flare'
import { useFlipTokStore } from '@/stores/fliptok'
import { usePicstagramStore } from '@/stores/picstagram'
import { useMediaStore } from '@/stores/media'
import { useMarketplaceStore } from '@/stores/marketplace'
import { useAppStoreStore } from '@/stores/app-store'
@@ -65,6 +66,8 @@ type AppMessage = {
| FlareEventData
| FlipTokVerificationData
| FlipTokNotificationData
| PicstagramVerificationData
| PicstagramNotificationData
| PhoneCall
| PhoneNotificationInput
| PhoneOpenPayload
@@ -143,6 +146,20 @@ type FlipTokNotificationData = {
title?: string
videoId?: string
}
type PicstagramVerificationData = {
profileId: string
verified: boolean
}
type PicstagramNotificationData = {
actor?: string
device?: PhoneNotificationDevicePayload
kind?: 'like' | 'comment' | 'follow' | 'follow_request' | 'verified'
postId?: string
text?: string
title?: string
}
const REFERENCE_VIEWPORT_WIDTH = 1920
const REFERENCE_VIEWPORT_HEIGHT = 1080
const PHONE_BASE_SCALE = 0.69
@@ -160,6 +177,7 @@ const messages = useMessagesStore()
const darkchat = useDarkChatStore()
const flare = useFlareStore()
const fliptok = useFlipTokStore()
const picstagram = usePicstagramStore()
const media = useMediaStore()
const marketplace = useMarketplaceStore()
const appStore = useAppStoreStore()
@@ -333,6 +351,32 @@ function onMessage(event: MessageEvent<AppMessage>): void {
}
notifications.show(notification)
if (phone.isOpen) void fliptok.loadActivities()
} else if (
event.data?.type === 'picstagram:verification-changed' &&
event.data.data
) {
const data = event.data.data as PicstagramVerificationData
picstagram.applyVerification(String(data.profileId), data.verified === true)
} else if (event.data?.type === 'picstagram:new' && event.data.data) {
const data = event.data.data as PicstagramNotificationData
const notification: PhoneNotificationInput = {
appId: 'picstagram',
subtitle: data.actor,
text: data.text ?? phone.t('Apps.picstagram.notifications.default'),
title: data.title ?? phone.t('Apps.picstagram.name'),
}
if (
data.device &&
(!phone.isOpen || data.device.imei !== phone.device?.imei)
) {
notification.device = {
imei: data.device.imei,
name: data.device.name,
preferences: parsePhonePreferences(data.device.settings ?? null),
}
}
notifications.show(notification)
if (phone.isOpen) void picstagram.loadActivities()
} else if (
event.data?.type === 'marketplace:new-message' &&
event.data.data
Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

+6 -5
View File
@@ -124,11 +124,12 @@ describe('app registry', () => {
PHONE_APPS.filter((app) => app.category === 'social').map(
(app) => app.id,
),
).toEqual([
'fliptok',
'flare',
'radio',
'local-pages',
).toEqual([
'picstagram',
'fliptok',
'flare',
'radio',
'local-pages',
'phone',
'darkchat',
'banking',
+15
View File
@@ -57,6 +57,7 @@ import citymarktIcon from '@/assets/img/app-icons/citymarkt.webp'
import localPagesIcon from '@/assets/img/app-icons/local-pages.webp'
import flareIcon from '@/assets/img/app-icons/flare.svg'
import flipTokIcon from '@/assets/img/app-icons/fliptok.webp'
import picstagramIcon from '@/assets/img/app-icons/picstagram.webp'
import type {
LaunchablePhoneAppDefinition,
LaunchablePhoneAppId,
@@ -64,6 +65,20 @@ import type {
} from '@/types/apps'
export const PHONE_APPS: PhoneAppDefinition[] = [
{
category: 'social',
component: markRaw(
defineAsyncComponent(() => import('@/views/apps/PicstagramApp.vue')),
),
dockOrder: null,
gridOrder: 23,
icon: markRaw(Camera),
iconClass: 'app-icon--picstagram',
iconImage: picstagramIcon,
id: 'picstagram',
labelKey: 'Apps.picstagram.name',
route: '/apps/picstagram',
},
{
category: 'social',
component: markRaw(
+198
View File
@@ -191,6 +191,204 @@ const defaultLocales: LocaleTree = {
default: 'Flare could not complete the request.',
},
},
picstagram: {
name: 'Picstagram',
loading: 'Loading Picstagram',
home: 'Home',
explore: 'Explore',
create: 'Create',
activity: 'Activity',
profile: 'Profile',
following: 'Following',
city: 'City',
posts: 'Posts',
followers: 'Followers',
verified: 'Verified profile',
editProfile: 'Edit profile',
saved: 'Saved',
photos: 'Photos',
emptyBio: 'No bio yet.',
authTitle: 'Your Picstagram profile',
login: 'Sign In',
register: 'Register',
createAccount: 'Create Profile',
logout: 'Sign Out',
signingOut: 'Signing Out...',
loginBody:
'Sign in to continue with your photos, stories, follows, and saved posts.',
registerBody: 'Create a private Picstagram login for your photo profile.',
registrationHint:
'An iFruit account is required once to create and own a Picstagram profile.',
displayName: 'Name',
username: 'Username',
password: 'Password',
confirmPassword: 'Confirm password',
passwordsMismatch: 'The passwords do not match.',
displayNamePlaceholder: 'Your name',
usernamePlaceholder: 'username',
passwordPlaceholder: 'At least 8 characters',
confirmPasswordPlaceholder: 'Enter password again',
signOutTitle: 'Sign out of Picstagram?',
signOutBody:
'Your profile and posts stay online. This phone returns to the sign-in screen.',
stories: 'Stories',
yourStory: 'Your story',
noStories: 'No active stories',
storyViews: '{count} views',
storyViewers: 'Story viewers',
seenBy: 'Seen by {count}',
nextStory: 'Next story',
closeStory: 'Close story',
removeStory: 'Remove story',
storyRemoved: 'Story removed.',
emptyFeed: 'Your feed is quiet',
emptyFeedBody: 'Discover profiles or share the first photo.',
discoverPeople: 'Discover people',
searchPlaceholder: 'Search profiles, captions, places, or tags',
noResults: 'No results',
noResultsBody: 'Try another username, caption, place, or hashtag.',
newPost: 'New Post',
newStory: 'New Story',
post: 'Post',
story: 'Story',
choosePhotos: 'Choose photos',
choosePhoto: 'Choose photo',
choosePhotosHint: 'Select up to five photos from Gallery',
chooseStoryHint: 'Select one photo from Gallery',
selectedPhotos: '{count} selected',
changePhotos: 'Change selection',
caption: 'Caption',
captionPlaceholder: 'Write a caption...',
storyTextPlaceholder: 'Add a short story text...',
location: 'Location',
locationPlaceholder: 'Add a place',
allowComments: 'Allow comments',
publishing: 'Publishing...',
published: 'Your post is live.',
storyPublished: 'Your story is live.',
comments: 'Comments',
comment: 'Comment',
noComments: 'No comments yet',
addComment: 'Add a comment...',
reply: 'Reply',
removeComment: 'Remove comment',
likes: '{count} likes',
viewComments: 'View all {count} comments',
like: 'Like',
unlike: 'Unlike',
save: 'Save',
unsave: 'Remove from saved',
more: 'More',
archive: 'Archive',
restore: 'Restore',
deletePost: 'Delete post',
deletePostTitle: 'Delete this post?',
deletePostBody: 'The post and its reactions will no longer be available.',
privateProfile: 'This profile is private',
privateProfileBody: 'Follow this profile to see its posts and stories.',
follow: 'Follow',
requested: 'Requested',
unfollow: 'Following',
accept: 'Accept',
decline: 'Decline',
followRequests: 'Follow requests',
noActivity: 'No activity yet',
markRead: 'Mark as read',
publicProfile: 'Public profile',
privateProfileSetting: 'Private profile',
privacyHint: 'Only accepted followers can see your posts and stories.',
bio: 'Bio',
avatar: 'Profile photo',
chooseAvatar: 'Choose profile photo',
removeAvatar: 'Remove profile photo',
saveProfile: 'Save profile',
profileSaved: 'Profile updated.',
cancel: 'Cancel',
done: 'Done',
report: 'Report',
reportTarget: 'Report {target}',
reportReason: 'Reason',
reportDetails: 'Additional details (optional)',
submitReport: 'Submit report',
reported: 'Report submitted.',
block: 'Block profile',
unblock: 'Unblock profile',
blocked: 'Profile blocked.',
blockTitle: 'Block @{handle}?',
blockBody:
"You will no longer see or interact with each other's Picstagram content.",
moderation: 'Moderation',
reports: 'Open reports',
noReports: 'No open reports',
noDetails: 'No additional details',
hide: 'Hide',
remove: 'Remove',
restoreAction: 'Restore',
dismiss: 'Dismiss',
reportTargets: {
post: 'Post',
profile: 'Profile',
comment: 'Comment',
story: 'Story',
},
reportReasons: {
spam: 'Spam or misleading',
harassment: 'Harassment or bullying',
dangerous: 'Dangerous activity',
illegal: 'Illegal content',
other: 'Something else',
},
activityKinds: {
follow_request: 'requested to follow you',
follow: 'started following you',
request_accepted: 'accepted your follow request',
like: 'liked your post',
comment: 'commented on your post',
verified: 'verification changed',
},
notifications: {
follow_request: '{actor} requested to follow you.',
follow: '{actor} started following you.',
request_accepted: '{actor} accepted your follow request.',
like: '{actor} liked your post.',
comment: '{actor} commented on your post.',
verified: 'Your Picstagram profile is now verified.',
default: 'You have new Picstagram activity.',
},
errors: {
invalid_post: 'Check the post details.',
invalid_story: 'Check the story details.',
invalid_media: 'Choose photos owned by this phone.',
invalid_comment: 'Enter a valid comment.',
comments_disabled: 'Comments are disabled.',
invalid_profile: 'Check your profile details.',
invalid_handle: 'Use 324 letters, numbers, dots, or underscores.',
invalid_display_name: 'Enter a display name.',
invalid_password: 'Password must be 872 characters.',
invalid_credentials: 'Username or password is incorrect.',
already_registered:
'This iFruit account already owns a Picstagram profile.',
handle_taken: 'This username is already taken.',
post_not_found: 'This post is unavailable.',
story_not_found: 'This story is unavailable.',
profile_not_found: 'This profile is unavailable.',
request_not_found: 'This follow request is no longer open.',
profile_unavailable: 'This profile is unavailable.',
blocked: 'This profile is blocked.',
already_reported: 'You already reported this content.',
invalid_report: 'Choose a valid report reason.',
invalid_search: 'Enter a valid search.',
invalid_cursor: 'The feed changed. Refresh and try again.',
invalid_status: 'This action is not available.',
not_authorized: 'You are not allowed to do that.',
report_not_found: 'This report is no longer open.',
rate_limited: 'Too many actions. Try again shortly.',
not_authenticated: 'Sign in to iFruit first.',
picstagram_not_authenticated: 'Sign in to Picstagram first.',
request_failed: 'The request failed.',
default: 'Picstagram could not complete the request.',
},
},
fliptok: {
name: 'FlipTok',
loading: 'Loading FlipTok',
+216
View File
@@ -0,0 +1,216 @@
import { createPinia, setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { usePicstagramStore } from '@/stores/picstagram'
import type {
PicstagramActivity,
PicstagramComment,
PicstagramPost,
PicstagramProfile,
PicstagramStory,
} from '@/types/picstagram'
import { nuiCall } from '@/utils/nui'
vi.mock('@/utils/nui', () => ({
nuiCall: vi.fn(),
}))
const profile: PicstagramProfile = {
avatar_media_id: null,
avatar_url: null,
bio: 'City light collector.',
display_name: 'Nova',
follow_status: null,
followers: 12,
following: 4,
handle: 'nova',
id: 'profile-1',
is_following: false,
is_owner: true,
is_requested: false,
locked: false,
post_count: 1,
private: false,
status: 'active',
verified: false,
}
const post: PicstagramPost = {
avatar_url: null,
caption: 'Los Santos after rain.',
comment_count: 1,
comments_enabled: true,
created_at: 1,
display_name: 'Nova',
handle: 'nova',
id: 'post-1',
is_liked: false,
is_owner: true,
is_saved: false,
like_count: 2,
location: 'Downtown',
media: [{ id: 1, position: 0, url: 'https://example.com/photo.webp' }],
private: false,
profile_id: profile.id,
verified: false,
}
const comment: PicstagramComment = {
avatar_url: null,
body: 'Beautiful.',
created_at: 1,
display_name: 'Nova',
handle: 'nova',
id: 'comment-1',
is_owner: true,
parent_id: null,
profile_id: profile.id,
verified: false,
}
const activity: PicstagramActivity = {
avatar_url: null,
created_at: 1,
display_name: 'Nova',
handle: 'nova',
id: 'activity-1',
kind: 'follow',
post_id: null,
post_url: null,
profile_id: profile.id,
read_at: null,
verified: false,
}
const story: PicstagramStory = {
avatar_url: null,
body: 'Tonight.',
created_at: 1,
display_name: 'Nova',
expires_at: 2,
handle: 'nova',
id: 'story-1',
is_owner: true,
profile_id: profile.id,
seen: true,
url: 'https://example.com/story.webp',
verified: false,
view_count: 3,
}
describe('Picstagram store', () => {
beforeEach(() => {
setActivePinia(createPinia())
vi.mocked(nuiCall).mockReset()
})
it('updates verification on every visible surface', () => {
const store = usePicstagramStore()
store.profile = { ...profile }
store.feed = [{ ...post }]
store.explore = [{ ...post }]
store.profilePosts = [{ ...post }]
store.saved = [{ ...post }]
store.searchPosts = [{ ...post }]
store.comments = [{ ...comment }]
store.activities = [{ ...activity }]
store.applyVerification(profile.id, true)
expect(store.profile.verified).toBe(true)
expect(
store
.allPostCollections()
.flat()
.every((item) => item.verified),
).toBe(true)
expect(store.comments[0].verified).toBe(true)
expect(store.activities[0].verified).toBe(true)
})
it('rolls back an optimistic like when the server rejects it', async () => {
vi.mocked(nuiCall).mockResolvedValue({ success: false })
const store = usePicstagramStore()
store.feed = [{ ...post }]
store.explore = [{ ...post }]
expect(await store.react(store.feed[0], 'like')).toBe(false)
expect(store.feed[0].is_liked).toBe(false)
expect(store.feed[0].like_count).toBe(2)
expect(store.explore[0].is_liked).toBe(false)
expect(store.explore[0].like_count).toBe(2)
})
it('removes a blocked profile from all local surfaces', async () => {
vi.mocked(nuiCall).mockResolvedValue({ success: true })
const store = usePicstagramStore()
store.feed = [{ ...post }]
store.explore = [{ ...post }]
store.profilePosts = [{ ...post }]
store.saved = [{ ...post }]
store.searchPosts = [{ ...post }]
store.comments = [{ ...comment }]
store.activities = [{ ...activity }]
store.stories = [{ ...story }]
store.viewedProfile = { ...profile, is_owner: false }
expect(await store.blockProfile(profile.id)).toBe(true)
expect(store.allPostCollections().flat()).toEqual([])
expect(store.comments).toEqual([])
expect(store.activities).toEqual([])
expect(store.stories).toEqual([])
expect(store.viewedProfile).toBeNull()
})
it('keeps the app signed out when no Picstagram session exists', async () => {
vi.mocked(nuiCall).mockResolvedValue({
success: true,
data: { authenticated: false, isAdmin: false },
})
const store = usePicstagramStore()
expect(await store.bootstrap()).toBe(true)
expect(store.authenticated).toBe(false)
expect(store.profile).toBeNull()
expect(store.feed).toEqual([])
})
it('hydrates the profile and stories after login', async () => {
vi.mocked(nuiCall)
.mockResolvedValueOnce({ success: true })
.mockResolvedValueOnce({
success: true,
data: {
authenticated: true,
feed: { hasMore: false, items: [{ ...post }], nextCursor: null },
isAdmin: false,
profile: { ...profile },
},
})
.mockResolvedValueOnce({ success: true, data: [{ ...story }] })
const store = usePicstagramStore()
expect((await store.login('nova', 'password123')).success).toBe(true)
expect(store.profile?.handle).toBe('nova')
expect(store.stories).toHaveLength(1)
expect(nuiCall).toHaveBeenNthCalledWith(1, 'picstagram:login', {
handle: 'nova',
password: 'password123',
})
})
it('clears all local state after logout', async () => {
vi.mocked(nuiCall).mockResolvedValue({ success: true })
const store = usePicstagramStore()
store.authenticated = true
store.profile = { ...profile }
store.feed = [{ ...post }]
store.stories = [{ ...story }]
expect((await store.logout()).success).toBe(true)
expect(store.authenticated).toBe(false)
expect(store.profile).toBeNull()
expect(store.feed).toEqual([])
expect(store.stories).toEqual([])
})
})
+399
View File
@@ -0,0 +1,399 @@
import { defineStore } from 'pinia'
import type {
PicstagramActivity,
PicstagramComment,
PicstagramPage,
PicstagramPost,
PicstagramProfile,
PicstagramProfilePage,
PicstagramReport,
PicstagramReportReason,
PicstagramReportTarget,
PicstagramSearchResult,
PicstagramStory,
PicstagramStoryViewer,
} from '@/types/picstagram'
import { nuiCall, type NuiResponse } from '@/utils/nui'
function replaceProfileState(
posts: PicstagramPost[],
profileId: string,
update: (post: PicstagramPost) => void,
): void {
posts.filter((post) => post.profile_id === profileId).forEach(update)
}
export const usePicstagramStore = defineStore('picstagram', {
state: () => ({
activities: [] as PicstagramActivity[],
authenticated: false,
comments: [] as PicstagramComment[],
explore: [] as PicstagramPost[],
exploreCursor: null as string | null,
feed: [] as PicstagramPost[],
feedCursor: null as string | null,
isAdmin: false,
loading: false,
profile: null as PicstagramProfile | null,
profilePosts: [] as PicstagramPost[],
reports: [] as PicstagramReport[],
saved: [] as PicstagramPost[],
searchPosts: [] as PicstagramPost[],
searchProfiles: [] as PicstagramProfile[],
stories: [] as PicstagramStory[],
storyViewers: [] as PicstagramStoryViewer[],
viewedProfile: null as PicstagramProfile | null,
}),
actions: {
allPostCollections(): PicstagramPost[][] {
return [
this.feed,
this.explore,
this.profilePosts,
this.saved,
this.searchPosts,
]
},
applyVerification(profileId: string, verified: boolean): void {
if (this.profile?.id === profileId) this.profile.verified = verified
if (this.viewedProfile?.id === profileId)
this.viewedProfile.verified = verified
this.allPostCollections().forEach((posts) =>
replaceProfileState(posts, profileId, (post) => {
post.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<{
authenticated: boolean
feed?: PicstagramPage
isAdmin: boolean
profile?: PicstagramProfile
}>('picstagram:bootstrap')
this.loading = false
if (!response.success || !response.data) return false
this.authenticated = response.data.authenticated
this.isAdmin = response.data.isAdmin
this.profile = response.data.profile ?? null
this.feed = response.data.feed?.items ?? []
this.feedCursor = response.data.feed?.nextCursor ?? null
if (this.authenticated) await this.loadStories()
return true
},
async login(handle: string, password: string): Promise<NuiResponse> {
const response = await nuiCall('picstagram:login', { handle, password })
if (response.success) await this.bootstrap()
return response
},
async register(
displayName: string,
handle: string,
password: string,
): Promise<NuiResponse> {
const response = await nuiCall('picstagram:register', {
displayName,
handle,
password,
})
if (response.success) await this.bootstrap()
return response
},
async logout(): Promise<NuiResponse> {
const response = await nuiCall('picstagram:logout')
if (response.success) this.$reset()
return response
},
async loadFeed(append = false): Promise<boolean> {
const response = await nuiCall<PicstagramPage>('picstagram:feed', {
cursor: append ? this.feedCursor : undefined,
})
if (!response.success || !response.data) return false
this.feed = append
? [...this.feed, ...response.data.items]
: response.data.items
this.feedCursor = response.data.nextCursor
return true
},
async loadExplore(append = false): Promise<boolean> {
const response = await nuiCall<PicstagramPage>('picstagram:explore', {
cursor: append ? this.exploreCursor : undefined,
})
if (!response.success || !response.data) return false
this.explore = append
? [...this.explore, ...response.data.items]
: response.data.items
this.exploreCursor = response.data.nextCursor
return true
},
async search(search: string): Promise<boolean> {
if (!search.trim()) {
this.searchPosts = []
this.searchProfiles = []
return true
}
const response = await nuiCall<PicstagramSearchResult>(
'picstagram:search',
{ search },
)
if (!response.success || !response.data) return false
this.searchPosts = response.data.posts
this.searchProfiles = response.data.profiles
return true
},
async loadSaved(): Promise<boolean> {
const response = await nuiCall<PicstagramPage>('picstagram:saved')
if (!response.success || !response.data) return false
this.saved = response.data.items
return true
},
async loadProfile(query: {
handle?: string
profileId?: string
}): Promise<boolean> {
const response = await nuiCall<PicstagramProfilePage>(
'picstagram:profile',
query,
)
if (!response.success || !response.data) return false
this.viewedProfile = response.data.profile
this.profilePosts = response.data.posts.items
return true
},
showOwnProfile(): void {
this.viewedProfile = null
this.profilePosts = this.feed.filter((post) => post.is_owner)
},
async updateProfile(payload: {
avatarMediaId: number
bio: string
displayName: string
handle: string
private: boolean
}): Promise<NuiResponse<PicstagramProfile>> {
const response = await nuiCall<PicstagramProfile>(
'picstagram:update-profile',
payload,
)
if (response.success && response.data) this.profile = response.data
return response
},
async publishPost(payload: {
caption: string
commentsEnabled: boolean
location: string
mediaIds: number[]
}): Promise<NuiResponse<{ id: string }>> {
const response = await nuiCall<{ id: string }>(
'picstagram:publish-post',
payload,
)
if (response.success) await this.loadFeed()
return response
},
async setPostStatus(
post: PicstagramPost,
status: 'archived' | 'published' | 'removed',
): Promise<boolean> {
const response = await nuiCall('picstagram:set-post-status', {
id: post.id,
status,
})
if (!response.success) return false
this.allPostCollections().forEach((posts) => {
const index = posts.findIndex((item) => item.id === post.id)
if (index >= 0) posts.splice(index, 1)
})
return true
},
async react(post: PicstagramPost, kind: 'like' | 'save'): Promise<boolean> {
const key = kind === 'like' ? 'is_liked' : 'is_saved'
const next = !post[key]
const matching = this.allPostCollections()
.flat()
.filter((item) => item.id === post.id)
matching.forEach((item) => {
item[key] = next
if (kind === 'like') item.like_count += next ? 1 : -1
})
const response = await nuiCall('picstagram:react', {
active: next,
id: post.id,
kind,
})
if (response.success) return true
matching.forEach((item) => {
item[key] = !next
if (kind === 'like') item.like_count += next ? -1 : 1
})
return false
},
async followProfile(profile: PicstagramProfile): Promise<boolean> {
const active = !profile.is_following && !profile.is_requested
const response = await nuiCall<{
status: 'accepted' | 'pending' | false
}>('picstagram:follow', { active, profileId: profile.id })
if (!response.success || !response.data) return false
const previousFollowing = profile.is_following
profile.follow_status = response.data.status || null
profile.is_following = response.data.status === 'accepted'
profile.is_requested = response.data.status === 'pending'
if (previousFollowing !== profile.is_following)
profile.followers += profile.is_following ? 1 : -1
return true
},
async respondFollow(profileId: string, accept: boolean): Promise<boolean> {
const response = await nuiCall('picstagram:respond-follow', {
accept,
profileId,
})
if (response.success)
this.activities = this.activities.filter(
(activity) =>
activity.kind !== 'follow_request' ||
activity.profile_id !== profileId,
)
return response.success
},
async loadComments(id: string): Promise<boolean> {
const response = await nuiCall<PicstagramComment[]>(
'picstagram:comments',
{ id },
)
this.comments = response.success && response.data ? response.data : []
return response.success
},
async comment(
id: string,
body: string,
parentId?: string,
): Promise<NuiResponse> {
return nuiCall('picstagram:comment', { body, id, parentId })
},
async removeComment(id: string): Promise<boolean> {
const response = await nuiCall('picstagram:remove-comment', { id })
if (response.success)
this.comments = this.comments.filter((comment) => comment.id !== id)
return response.success
},
async loadStories(): Promise<boolean> {
const response = await nuiCall<PicstagramStory[]>('picstagram:stories')
this.stories = response.success && response.data ? response.data : []
return response.success
},
async publishStory(
mediaId: number,
body: string,
): Promise<NuiResponse<{ id: string }>> {
const response = await nuiCall<{ id: string }>(
'picstagram:publish-story',
{ body, mediaId },
)
if (response.success) await this.loadStories()
return response
},
async viewStory(story: PicstagramStory): Promise<void> {
if (!story.seen && !story.is_owner) {
const response = await nuiCall('picstagram:view-story', {
id: story.id,
})
if (response.success) story.seen = true
}
},
async loadStoryViewers(id: string): Promise<boolean> {
const response = await nuiCall<PicstagramStoryViewer[]>(
'picstagram:story-viewers',
{ id },
)
this.storyViewers = response.success && response.data ? response.data : []
return response.success
},
async removeStory(id: string): Promise<boolean> {
const response = await nuiCall('picstagram:remove-story', { id })
if (response.success)
this.stories = this.stories.filter((story) => story.id !== id)
return response.success
},
async loadActivities(): Promise<boolean> {
const response = await nuiCall<PicstagramActivity[]>(
'picstagram:activities',
)
this.activities = response.success && response.data ? response.data : []
return response.success
},
async markActivities(): Promise<boolean> {
const response = await nuiCall('picstagram:mark-activities')
if (response.success)
this.activities.forEach((activity) => {
activity.read_at ??= new Date().toISOString()
})
return response.success
},
async blockProfile(profileId: string): Promise<boolean> {
const response = await nuiCall('picstagram:block', {
active: true,
profileId,
})
if (!response.success) return false
this.allPostCollections().forEach((posts) => {
for (let index = posts.length - 1; index >= 0; index -= 1)
if (posts[index].profile_id === profileId) posts.splice(index, 1)
})
this.comments = this.comments.filter(
(comment) => comment.profile_id !== profileId,
)
this.activities = this.activities.filter(
(activity) => activity.profile_id !== profileId,
)
this.stories = this.stories.filter(
(story) => story.profile_id !== profileId,
)
if (this.viewedProfile?.id === profileId) this.viewedProfile = null
return true
},
async report(
targetType: PicstagramReportTarget,
targetId: string,
reason: PicstagramReportReason,
details: string,
): Promise<NuiResponse> {
return nuiCall('picstagram:report', {
details,
reason,
targetId,
targetType,
})
},
async loadReports(): Promise<boolean> {
const response = await nuiCall<PicstagramReport[]>(
'picstagram:admin-reports',
)
this.reports = response.success && response.data ? response.data : []
return response.success
},
async resolveReport(
id: string,
action: 'dismiss' | 'hide' | 'remove' | 'restore',
): Promise<boolean> {
const response = await nuiCall('picstagram:admin-resolve-report', {
action,
id,
})
if (response.success)
this.reports = this.reports.filter((report) => report.id !== id)
return response.success
},
},
})
+1
View File
@@ -29,6 +29,7 @@ export type PhoneAppId =
| 'local-pages'
| 'flare'
| 'fliptok'
| 'picstagram'
export type LaunchablePhoneAppId = PhoneAppId
+141
View File
@@ -0,0 +1,141 @@
export type PicstagramProfile = {
avatar_media_id: number | null
avatar_url: string | null
bio: string
display_name: string
follow_status: 'accepted' | 'pending' | null
followers: number
following: number
handle: string
id: string
is_following: boolean
is_owner: boolean
is_requested: boolean
locked: boolean
post_count: number
private: boolean
status: 'active'
verified: boolean
}
export type PicstagramPostMedia = {
id: number
position: number
url: string
}
export type PicstagramPost = {
avatar_url: string | null
caption: string
comment_count: number
comments_enabled: boolean
created_at: number
display_name: string
handle: string
id: string
is_liked: boolean
is_owner: boolean
is_saved: boolean
like_count: number
location: string
media: PicstagramPostMedia[]
private: boolean
profile_id: string
verified: boolean
}
export type PicstagramPage = {
hasMore: boolean
items: PicstagramPost[]
nextCursor: string | null
}
export type PicstagramProfilePage = {
posts: PicstagramPage
profile: PicstagramProfile
}
export type PicstagramSearchResult = {
posts: PicstagramPost[]
profiles: PicstagramProfile[]
}
export type PicstagramComment = {
avatar_url: string | null
body: string
created_at: number
display_name: string
handle: string
id: string
is_owner: boolean
parent_id: string | null
profile_id: string
verified: boolean
}
export type PicstagramStory = {
avatar_url: string | null
body: string
created_at: number
display_name: string
expires_at: number
handle: string
id: string
is_owner: boolean
profile_id: string
seen: boolean
url: string
verified: boolean
view_count: number
}
export type PicstagramStoryViewer = {
avatar_url: string | null
created_at: number
display_name: string
handle: string
id: string
verified: boolean
}
export type PicstagramActivityKind =
| 'follow_request'
| 'follow'
| 'request_accepted'
| 'like'
| 'comment'
| 'verified'
export type PicstagramActivity = {
avatar_url: string | null
created_at: number
display_name: string
handle: string
id: string
kind: PicstagramActivityKind
post_id: string | null
post_url: string | null
profile_id: string
read_at: string | null
verified: boolean
}
export type PicstagramReportTarget = 'profile' | 'post' | 'story' | 'comment'
export type PicstagramReportReason =
| 'spam'
| 'harassment'
| 'dangerous'
| 'illegal'
| 'other'
export type PicstagramReport = {
created_at: number
details: string
id: string
reason: PicstagramReportReason
reporter_display_name: string
reporter_handle: string
target_id: string
target_type: PicstagramReportTarget
}
+1
View File
@@ -82,6 +82,7 @@ const DEFAULT_APP_NOTIFICATIONS: Record<
'neon-drop': { enabled: true, sounds: true },
citymarkt: { enabled: true, sounds: true },
'local-pages': { enabled: true, sounds: true },
picstagram: { enabled: true, sounds: true },
fliptok: { enabled: true, sounds: true },
camera: { enabled: true, sounds: true },
clock: { enabled: true, sounds: true },
File diff suppressed because it is too large Load Diff
+531
View File
@@ -176,6 +176,183 @@ let flipTokReports = [
creator_display_name: 'Nova',
},
]
let picstagramAuthenticated = true
const picstagramProfiles = [
{
avatar_media_id: null,
avatar_url:
'https://images.unsplash.com/photo-1534528741775-53994a69daeb?auto=format&fit=crop&w=240&q=80',
bio: 'Street light, rain, and the quiet side of Los Santos.',
display_name: 'Skyline',
follow_status: null,
followers: 1842,
following: 128,
handle: 'skyline',
id: 'pic-profile-1',
is_following: false,
is_owner: true,
is_requested: false,
locked: false,
post_count: 1,
private: false,
status: 'active',
verified: true,
},
{
avatar_media_id: null,
avatar_url:
'https://images.unsplash.com/photo-1494790108377-be9c29b29330?auto=format&fit=crop&w=240&q=80',
bio: 'Golden-hour wanderer.',
display_name: 'Nova',
follow_status: 'accepted',
followers: 9214,
following: 91,
handle: 'nova.ls',
id: 'pic-profile-2',
is_following: true,
is_owner: false,
is_requested: false,
locked: false,
post_count: 2,
private: false,
status: 'active',
verified: true,
},
{
avatar_media_id: null,
avatar_url: null,
bio: 'Private film diary.',
display_name: 'Milo Reed',
follow_status: null,
followers: 318,
following: 44,
handle: 'milo.reed',
id: 'pic-profile-3',
is_following: false,
is_owner: false,
is_requested: false,
locked: true,
post_count: 0,
private: true,
status: 'active',
verified: false,
},
]
let picstagramPosts = [
{
avatar_url: picstagramProfiles[1].avatar_url,
caption: 'The city holds its breath right before sunrise. #LosSantos',
comment_count: 2,
comments_enabled: true,
created_at: Date.now() - 32 * 60 * 1000,
display_name: picstagramProfiles[1].display_name,
handle: picstagramProfiles[1].handle,
id: 'pic-post-1',
is_liked: false,
is_owner: false,
is_saved: true,
like_count: 842,
location: 'Vinewood Hills',
media: [
{
id: 8101,
position: 0,
url: 'https://images.unsplash.com/photo-1518005020951-eccb494ad742?auto=format&fit=crop&w=900&q=85',
},
{
id: 8102,
position: 1,
url: 'https://images.unsplash.com/photo-1444723121867-7a241cacace9?auto=format&fit=crop&w=900&q=85',
},
],
private: false,
profile_id: picstagramProfiles[1].id,
verified: true,
},
{
avatar_url: picstagramProfiles[0].avatar_url,
caption: 'Neon after rain.',
comment_count: 1,
comments_enabled: true,
created_at: Date.now() - 3 * 60 * 60 * 1000,
display_name: picstagramProfiles[0].display_name,
handle: picstagramProfiles[0].handle,
id: 'pic-post-2',
is_liked: true,
is_owner: true,
is_saved: false,
like_count: 216,
location: 'Downtown Los Santos',
media: [
{
id: 8103,
position: 0,
url: 'https://images.unsplash.com/photo-1519608487953-e999c86e7455?auto=format&fit=crop&w=900&q=85',
},
],
private: false,
profile_id: picstagramProfiles[0].id,
verified: true,
},
]
let picstagramComments = [
{
avatar_url: picstagramProfiles[1].avatar_url,
body: 'That reflection is unreal.',
created_at: Date.now() - 18 * 60 * 1000,
display_name: picstagramProfiles[1].display_name,
handle: picstagramProfiles[1].handle,
id: 'pic-comment-1',
is_owner: false,
parent_id: null,
profile_id: picstagramProfiles[1].id,
verified: true,
},
]
let picstagramStories = [
{
avatar_url: picstagramProfiles[1].avatar_url,
body: 'First light over Vinewood.',
created_at: Date.now() - 12 * 60 * 1000,
display_name: picstagramProfiles[1].display_name,
expires_at: Date.now() + 22 * 60 * 60 * 1000,
handle: picstagramProfiles[1].handle,
id: 'pic-story-1',
is_owner: false,
profile_id: picstagramProfiles[1].id,
seen: false,
url: 'https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?auto=format&fit=crop&w=900&q=85',
verified: true,
view_count: 42,
},
]
let picstagramActivities = [
{
avatar_url: picstagramProfiles[1].avatar_url,
created_at: Date.now() - 9 * 60 * 1000,
display_name: picstagramProfiles[1].display_name,
handle: picstagramProfiles[1].handle,
id: 'pic-activity-1',
kind: 'like',
post_id: 'pic-post-2',
post_url: picstagramPosts[1].media[0].url,
profile_id: picstagramProfiles[1].id,
read_at: null,
verified: true,
},
]
let picstagramReports = [
{
created_at: Date.now() - 45 * 60 * 1000,
details: 'Please review the caption and location.',
id: 'pic-report-1',
reason: 'spam',
reporter_display_name: 'Skyline',
reporter_handle: 'skyline',
target_id: 'pic-post-1',
target_type: 'post',
},
]
const mockBankTransactions = [
{
id: 1,
@@ -1548,6 +1725,360 @@ app.post('/api/:endpoint', (request, response) => {
response.json({ success: true, data: message })
return
}
if (endpoint === 'picstagram:bootstrap') {
response.json({
success: true,
data: picstagramAuthenticated
? {
authenticated: true,
feed: {
hasMore: false,
items: picstagramPosts,
nextCursor: null,
},
isAdmin: true,
profile: picstagramProfiles[0],
}
: { authenticated: false, isAdmin: true },
})
return
}
if (endpoint === 'picstagram:login' || endpoint === 'picstagram:register') {
picstagramAuthenticated = true
response.json({ success: true })
return
}
if (endpoint === 'picstagram:logout') {
picstagramAuthenticated = false
response.json({ success: true })
return
}
if (endpoint === 'picstagram:feed') {
response.json({
success: true,
data: { hasMore: false, items: picstagramPosts, nextCursor: null },
})
return
}
if (endpoint === 'picstagram:explore') {
response.json({
success: true,
data: {
hasMore: false,
items: picstagramPosts.filter((post) => !post.is_owner),
nextCursor: null,
},
})
return
}
if (endpoint === 'picstagram:saved') {
response.json({
success: true,
data: {
hasMore: false,
items: picstagramPosts.filter((post) => post.is_saved),
nextCursor: null,
},
})
return
}
if (endpoint === 'picstagram:search') {
const search = String(request.body.search ?? '').toLowerCase()
response.json({
success: true,
data: {
posts: picstagramPosts.filter((post) =>
`${post.handle} ${post.display_name} ${post.caption} ${post.location}`
.toLowerCase()
.includes(search),
),
profiles: picstagramProfiles.filter((profile) =>
`${profile.handle} ${profile.display_name}`
.toLowerCase()
.includes(search),
),
},
})
return
}
if (endpoint === 'picstagram:profile') {
const profile = picstagramProfiles.find(
(item) =>
item.id === request.body.profileId ||
item.handle === String(request.body.handle ?? '').toLowerCase(),
)
if (!profile) {
response.json({ success: false, error: 'profile_not_found' })
return
}
response.json({
success: true,
data: {
posts: {
hasMore: false,
items: picstagramPosts.filter(
(post) => post.profile_id === profile.id,
),
nextCursor: null,
},
profile,
},
})
return
}
if (endpoint === 'picstagram:update-profile') {
Object.assign(picstagramProfiles[0], {
bio: request.body.bio,
display_name: request.body.displayName,
handle: request.body.handle,
private: request.body.private,
})
response.json({ success: true, data: picstagramProfiles[0] })
return
}
if (endpoint === 'picstagram:publish-post') {
const media = request.body.mediaIds
.map((id) => mockMedia.find((item) => item.id === id))
.filter((item) => item?.mediaType === 'photo')
if (!media.length) {
response.json({ success: false, error: 'invalid_media' })
return
}
const post = {
avatar_url: picstagramProfiles[0].avatar_url,
caption: request.body.caption,
comment_count: 0,
comments_enabled: request.body.commentsEnabled,
created_at: Date.now(),
display_name: picstagramProfiles[0].display_name,
handle: picstagramProfiles[0].handle,
id: `pic-post-${Date.now()}`,
is_liked: false,
is_owner: true,
is_saved: false,
like_count: 0,
location: request.body.location,
media: media.map((item, position) => ({
id: item.id,
position,
url: item.url,
})),
private: picstagramProfiles[0].private,
profile_id: picstagramProfiles[0].id,
verified: picstagramProfiles[0].verified,
}
picstagramPosts.unshift(post)
response.json({ success: true, data: { id: post.id } })
return
}
if (endpoint === 'picstagram:update-post') {
const post = picstagramPosts.find((item) => item.id === request.body.id)
if (post)
Object.assign(post, {
caption: request.body.caption,
comments_enabled: request.body.commentsEnabled,
location: request.body.location,
})
response.json({
success: Boolean(post),
error: post ? undefined : 'post_not_found',
})
return
}
if (endpoint === 'picstagram:set-post-status') {
if (request.body.status !== 'published')
picstagramPosts = picstagramPosts.filter(
(post) => post.id !== request.body.id,
)
response.json({ success: true })
return
}
if (endpoint === 'picstagram:react') {
const post = picstagramPosts.find((item) => item.id === request.body.id)
if (post) {
const key = request.body.kind === 'like' ? 'is_liked' : 'is_saved'
const changed = post[key] !== request.body.active
post[key] = request.body.active
if (changed && request.body.kind === 'like')
post.like_count += request.body.active ? 1 : -1
}
response.json({
success: Boolean(post),
error: post ? undefined : 'post_not_found',
})
return
}
if (endpoint === 'picstagram:follow') {
const profile = picstagramProfiles.find(
(item) => item.id === request.body.profileId,
)
if (!profile) {
response.json({ success: false, error: 'profile_not_found' })
return
}
profile.follow_status = request.body.active
? profile.private
? 'pending'
: 'accepted'
: null
profile.is_following = profile.follow_status === 'accepted'
profile.is_requested = profile.follow_status === 'pending'
profile.locked = profile.private && !profile.is_following
response.json({ success: true, data: { status: profile.follow_status } })
return
}
if (endpoint === 'picstagram:respond-follow') {
picstagramActivities = picstagramActivities.filter(
(activity) =>
activity.kind !== 'follow_request' ||
activity.profile_id !== request.body.profileId,
)
response.json({ success: true })
return
}
if (endpoint === 'picstagram:comments') {
response.json({ success: true, data: picstagramComments })
return
}
if (endpoint === 'picstagram:comment') {
const comment = {
avatar_url: picstagramProfiles[0].avatar_url,
body: request.body.body,
created_at: Date.now(),
display_name: picstagramProfiles[0].display_name,
handle: picstagramProfiles[0].handle,
id: `pic-comment-${Date.now()}`,
is_owner: true,
parent_id: request.body.parentId ?? null,
profile_id: picstagramProfiles[0].id,
verified: picstagramProfiles[0].verified,
}
picstagramComments.push(comment)
response.json({ success: true, data: { id: comment.id } })
return
}
if (endpoint === 'picstagram:remove-comment') {
picstagramComments = picstagramComments.filter(
(comment) => comment.id !== request.body.id,
)
response.json({ success: true })
return
}
if (endpoint === 'picstagram:stories') {
response.json({ success: true, data: picstagramStories })
return
}
if (endpoint === 'picstagram:publish-story') {
const media = mockMedia.find((item) => item.id === request.body.mediaId)
if (!media || media.mediaType !== 'photo') {
response.json({ success: false, error: 'invalid_media' })
return
}
const story = {
avatar_url: picstagramProfiles[0].avatar_url,
body: request.body.body,
created_at: Date.now(),
display_name: picstagramProfiles[0].display_name,
expires_at: Date.now() + 24 * 60 * 60 * 1000,
handle: picstagramProfiles[0].handle,
id: `pic-story-${Date.now()}`,
is_owner: true,
profile_id: picstagramProfiles[0].id,
seen: true,
url: media.url,
verified: picstagramProfiles[0].verified,
view_count: 0,
}
picstagramStories.unshift(story)
response.json({ success: true, data: { id: story.id } })
return
}
if (endpoint === 'picstagram:view-story') {
const story = picstagramStories.find((item) => item.id === request.body.id)
if (story) story.seen = true
response.json({ success: Boolean(story) })
return
}
if (endpoint === 'picstagram:story-viewers') {
response.json({
success: true,
data: [
{
avatar_url: picstagramProfiles[1].avatar_url,
created_at: Date.now() - 60000,
display_name: picstagramProfiles[1].display_name,
handle: picstagramProfiles[1].handle,
id: picstagramProfiles[1].id,
verified: true,
},
],
})
return
}
if (endpoint === 'picstagram:remove-story') {
picstagramStories = picstagramStories.filter(
(story) => story.id !== request.body.id,
)
response.json({ success: true })
return
}
if (endpoint === 'picstagram:activities') {
response.json({ success: true, data: picstagramActivities })
return
}
if (endpoint === 'picstagram:mark-activities') {
picstagramActivities.forEach((activity) => {
activity.read_at = new Date().toISOString()
})
response.json({ success: true })
return
}
if (endpoint === 'picstagram:block') {
const profileId = request.body.profileId
picstagramPosts = picstagramPosts.filter(
(post) => post.profile_id !== profileId,
)
picstagramComments = picstagramComments.filter(
(comment) => comment.profile_id !== profileId,
)
picstagramStories = picstagramStories.filter(
(story) => story.profile_id !== profileId,
)
picstagramActivities = picstagramActivities.filter(
(activity) => activity.profile_id !== profileId,
)
response.json({ success: true })
return
}
if (endpoint === 'picstagram:report') {
picstagramReports.unshift({
created_at: Date.now(),
details: request.body.details ?? '',
id: `pic-report-${Date.now()}`,
reason: request.body.reason,
reporter_display_name: picstagramProfiles[0].display_name,
reporter_handle: picstagramProfiles[0].handle,
target_id: request.body.targetId,
target_type: request.body.targetType,
})
response.json({ success: true })
return
}
if (endpoint === 'picstagram:admin-reports') {
response.json({ success: true, data: picstagramReports })
return
}
if (endpoint === 'picstagram:admin-resolve-report') {
picstagramReports = picstagramReports.filter(
(report) => report.id !== request.body.id,
)
response.json({ success: true })
return
}
if (endpoint.startsWith('picstagram:')) {
response.json({ success: true })
return
}
if (endpoint === 'fliptok:bootstrap') {
if (!flipTokAuthenticated) {
response.json({
+22
View File
@@ -283,6 +283,28 @@ Config.FlipTok = {
ReportAdminGroups = { "admin" },
}
Config.Picstagram = {
PageSize = 12,
CommentPageSize = 100,
MaxPostMedia = 5,
CaptionMaxLength = 800,
CommentMaxLength = 300,
BioMaxLength = 160,
LocationMaxLength = 80,
StoryTextMaxLength = 160,
StoryLifetimeSeconds = 24 * 60 * 60,
PasswordMinLength = 8,
PasswordMaxLength = 72,
PasswordPepperConvar = "sky_phone_picstagram_password_pepper",
PostsPerMinute = 6,
StoriesPerMinute = 6,
CommentsPerMinute = 20,
ReportDetailsMaxLength = 500,
ReportReasons = { "spam", "harassment", "dangerous", "illegal", "other" },
VerifyCommand = "picstagramverify",
AdminGroups = { "admin" },
}
Config.MapMarkers = {
MaximumMarkers = 50,
LabelMaxLength = 40,
+80
View File
@@ -8,6 +8,14 @@ Locales["en"] = {
verified = "verified",
unverified = "unverified",
},
PicstagramCommand = {
usage = "Usage: /{command} <@handle> <on|off>",
noPermission = "You do not have permission to manage Picstagram verification.",
notFound = "Picstagram profile @{handle} was not found.",
updated = "Picstagram @{handle} is now {state}.",
verified = "verified",
unverified = "unverified",
},
DeviceErrors = {
phone_slot_missing = "The used phone could not be identified. Make sure phones are not stacked.",
phone_stacked = "Phones cannot be stacked.",
@@ -102,6 +110,78 @@ Locales["en"] = {
match_not_found = "This match is no longer available.", invalid_message = "Write a message before sending.", invalid_attachment = "This attachment is unavailable.", gif_provider_unconfigured = "GIF search is not configured.", gif_provider_unauthorized = "The GIF provider key is invalid.", gif_provider_rate_limited = "GIF search is busy. Try again shortly.", gif_provider_failed = "GIFs are temporarily unavailable.", rate_limited = "Slow down for a moment and try again.", default = "Flare could not complete the request.",
},
},
picstagram = {
name = "Picstagram", loading = "Loading Picstagram", home = "Home", explore = "Explore",
create = "Create", activity = "Activity", profile = "Profile", following = "Following",
city = "City", posts = "Posts", followers = "Followers", verified = "Verified profile",
editProfile = "Edit profile", saved = "Saved", photos = "Photos", emptyBio = "No bio yet.",
authTitle = "Your Picstagram profile", login = "Sign In", register = "Register",
createAccount = "Create Profile", logout = "Sign Out", signingOut = "Signing Out...",
loginBody = "Sign in to continue with your photos, stories, follows, and saved posts.",
registerBody = "Create a private Picstagram login for your photo profile.",
registrationHint = "An iFruit account is required once to create and own a Picstagram profile.",
displayName = "Name", username = "Username", password = "Password",
confirmPassword = "Confirm password", passwordsMismatch = "The passwords do not match.",
displayNamePlaceholder = "Your name", usernamePlaceholder = "username",
passwordPlaceholder = "At least 8 characters", confirmPasswordPlaceholder = "Enter password again",
signOutTitle = "Sign out of Picstagram?",
signOutBody = "Your profile and posts stay online. This phone returns to the sign-in screen.",
stories = "Stories", yourStory = "Your story", noStories = "No active stories",
storyViews = "{count} views", storyViewers = "Story viewers", seenBy = "Seen by {count}",
nextStory = "Next story", closeStory = "Close story", removeStory = "Remove story", storyRemoved = "Story removed.",
emptyFeed = "Your feed is quiet", emptyFeedBody = "Discover profiles or share the first photo.",
discoverPeople = "Discover people", searchPlaceholder = "Search profiles, captions, places, or tags",
noResults = "No results", noResultsBody = "Try another username, caption, place, or hashtag.",
newPost = "New Post", newStory = "New Story", post = "Post", story = "Story",
choosePhotos = "Choose photos", choosePhoto = "Choose photo",
choosePhotosHint = "Select up to five photos from Gallery", chooseStoryHint = "Select one photo from Gallery",
selectedPhotos = "{count} selected", changePhotos = "Change selection", caption = "Caption",
captionPlaceholder = "Write a caption...", storyTextPlaceholder = "Add a short story text...",
location = "Location", locationPlaceholder = "Add a place", allowComments = "Allow comments",
publishing = "Publishing...", published = "Your post is live.", storyPublished = "Your story is live.",
comments = "Comments", comment = "Comment", noComments = "No comments yet", addComment = "Add a comment...",
reply = "Reply", removeComment = "Remove comment", likes = "{count} likes",
viewComments = "View all {count} comments", like = "Like", unlike = "Unlike", save = "Save",
unsave = "Remove from saved", more = "More", archive = "Archive", restore = "Restore",
deletePost = "Delete post", deletePostTitle = "Delete this post?",
deletePostBody = "The post and its reactions will no longer be available.",
privateProfile = "This profile is private", privateProfileBody = "Follow this profile to see its posts and stories.",
follow = "Follow", requested = "Requested", unfollow = "Following", accept = "Accept", decline = "Decline",
followRequests = "Follow requests", noActivity = "No activity yet", markRead = "Mark as read",
publicProfile = "Public profile", privateProfileSetting = "Private profile",
privacyHint = "Only accepted followers can see your posts and stories.", bio = "Bio",
avatar = "Profile photo", chooseAvatar = "Choose profile photo", removeAvatar = "Remove profile photo",
saveProfile = "Save profile", profileSaved = "Profile updated.", cancel = "Cancel", done = "Done",
report = "Report", reportTarget = "Report {target}", reportReason = "Reason",
reportDetails = "Additional details (optional)", submitReport = "Submit report", reported = "Report submitted.",
block = "Block profile", unblock = "Unblock profile", blocked = "Profile blocked.",
blockTitle = "Block @{handle}?", blockBody = "You will no longer see or interact with each other's Picstagram content.",
moderation = "Moderation", reports = "Open reports", noReports = "No open reports",
noDetails = "No additional details", hide = "Hide", remove = "Remove", restoreAction = "Restore", dismiss = "Dismiss",
reportTargets = { post = "Post", profile = "Profile", comment = "Comment", story = "Story" },
reportReasons = { spam = "Spam or misleading", harassment = "Harassment or bullying", dangerous = "Dangerous activity", illegal = "Illegal content", other = "Something else" },
activityKinds = { follow_request = "requested to follow you", follow = "started following you", request_accepted = "accepted your follow request", like = "liked your post", comment = "commented on your post", verified = "verification changed" },
notifications = { follow_request = "{actor} requested to follow you.", follow = "{actor} started following you.", request_accepted = "{actor} accepted your follow request.", like = "{actor} liked your post.", comment = "{actor} commented on your post.", verified = "Your Picstagram profile is now verified.", default = "You have new Picstagram activity." },
errors = {
invalid_post = "Check the post details.", invalid_story = "Check the story details.",
invalid_media = "Choose photos owned by this phone.", invalid_comment = "Enter a valid comment.",
comments_disabled = "Comments are disabled.", invalid_profile = "Check your profile details.",
invalid_handle = "Use 324 letters, numbers, dots, or underscores.",
invalid_display_name = "Enter a display name.", invalid_password = "Password must be 872 characters.",
invalid_credentials = "Username or password is incorrect.",
already_registered = "This iFruit account already owns a Picstagram profile.",
handle_taken = "This username is already taken.", post_not_found = "This post is unavailable.",
story_not_found = "This story is unavailable.", profile_not_found = "This profile is unavailable.",
request_not_found = "This follow request is no longer open.", profile_unavailable = "This profile is unavailable.",
blocked = "This profile is blocked.", already_reported = "You already reported this content.",
invalid_report = "Choose a valid report reason.", invalid_search = "Enter a valid search.",
invalid_cursor = "The feed changed. Refresh and try again.", invalid_status = "This action is not available.",
not_authorized = "You are not allowed to do that.", report_not_found = "This report is no longer open.",
rate_limited = "Too many actions. Try again shortly.", not_authenticated = "Sign in to iFruit first.",
picstagram_not_authenticated = "Sign in to Picstagram first.", request_failed = "The request failed.",
default = "Picstagram could not complete the request."
},
},
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",
+1
View File
@@ -59,6 +59,7 @@ server_scripts {
'source/server/marketplace.lua',
'source/server/pages.lua',
'source/server/fliptok.lua',
'source/server/picstagram.lua',
'source/server/map.lua',
'source/server/calendar.lua',
'source/server/radio.lua',
+46
View File
@@ -82,6 +82,36 @@ local server_callbacks = {
"fliptok:admin-resolve-report",
"fliptok:block",
"fliptok:delete",
"picstagram:register",
"picstagram:login",
"picstagram:logout",
"picstagram:bootstrap",
"picstagram:feed",
"picstagram:explore",
"picstagram:search",
"picstagram:saved",
"picstagram:profile",
"picstagram:update-profile",
"picstagram:publish-post",
"picstagram:update-post",
"picstagram:set-post-status",
"picstagram:react",
"picstagram:follow",
"picstagram:respond-follow",
"picstagram:comments",
"picstagram:comment",
"picstagram:remove-comment",
"picstagram:publish-story",
"picstagram:stories",
"picstagram:view-story",
"picstagram:story-viewers",
"picstagram:remove-story",
"picstagram:activities",
"picstagram:mark-activities",
"picstagram:block",
"picstagram:report",
"picstagram:admin-reports",
"picstagram:admin-resolve-report",
"calendar:list",
"calendar:create",
"calendar:update",
@@ -450,6 +480,22 @@ RegisterNetEvent("sky_phone:fliptok:new", function(data)
SendNUIMessage({ type = "fliptok:new", data = data })
end)
RegisterNetEvent("sky_phone:picstagram:command-feedback", function(data)
Bridge.Framework.Notify("Picstagram", data.message, data.notificationType, 5000)
end)
RegisterNetEvent("sky_phone:picstagram:verification-changed", function(data)
SendNUIMessage({ type = "picstagram:verification-changed", data = data })
end)
RegisterNetEvent("sky_phone:picstagram:new", function(data)
local picstagram_locale = get_locale().Nui.Apps.picstagram
local notification_text = picstagram_locale.notifications[data.kind] or picstagram_locale.notifications.default
data.title = picstagram_locale.name
data.text = notification_text:gsub("{actor}", tostring(data.actor or ""))
SendNUIMessage({ type = "picstagram:new", data = data })
end)
RegisterNetEvent("sky_phone:marketplace:new-message", function(data)
local marketplace_locale = get_locale().Nui.Apps.citymarkt
data.title = marketplace_locale.name
+293
View File
@@ -908,6 +908,299 @@ local schema = {
},
tableOptions = "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
},
{
name = "sky_phone_picstagram_profiles",
columns = {
{ name = "id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
{ name = "account_id", type = "BIGINT UNSIGNED NOT NULL" },
{ name = "handle", type = "VARCHAR(24) NOT NULL", characterSet = "ascii", collation = "ascii_general_ci" },
{ name = "display_name", type = "VARCHAR(40) NOT NULL" },
{ name = "bio", type = "VARCHAR(160) NOT NULL DEFAULT ''" },
{ name = "avatar_media_id", type = "BIGINT UNSIGNED NULL" },
{ name = "private", type = "TINYINT(1) NOT NULL DEFAULT 0" },
{ name = "verified", type = "TINYINT(1) NOT NULL DEFAULT 0" },
{ name = "status", type = "ENUM('active', 'hidden', 'removed') NOT NULL DEFAULT 'active'" },
{ name = "created_at", type = "DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP" },
{ name = "updated_at", type = "DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" },
},
primaryKey = "id",
uniqueKeys = {
{ name = "uniq_sky_phone_picstagram_account", columns = "(`account_id`)" },
{ name = "uniq_sky_phone_picstagram_handle", columns = "(`handle`)" },
},
foreignKeys = {
{ column = "account_id", references = "`sky_phone_accounts` (`id`) ON DELETE CASCADE" },
{ column = "avatar_media_id", references = "`sky_phone_media` (`id`) ON DELETE SET NULL" },
},
tableOptions = "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
},
{
name = "sky_phone_picstagram_credentials",
columns = {
{ name = "profile_id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
{ name = "password_hash", type = "BINARY(32) NOT NULL" },
{ name = "password_salt", type = "CHAR(32) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
{ name = "created_at", type = "DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP" },
{ name = "updated_at", type = "DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" },
},
primaryKey = "profile_id",
foreignKeys = {
{ column = "profile_id", references = "`sky_phone_picstagram_profiles` (`id`) ON DELETE CASCADE" },
},
tableOptions = "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
},
{
name = "sky_phone_picstagram_sessions",
columns = {
{ name = "device_imei", type = "CHAR(15) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
{ name = "profile_id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
{ name = "created_at", type = "DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP" },
{ name = "updated_at", type = "DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" },
},
primaryKey = "device_imei",
indexes = {
{ name = "idx_sky_phone_picstagram_sessions_profile", columns = "(`profile_id`, `updated_at`)" },
},
foreignKeys = {
{ column = "device_imei", references = "`sky_phone_devices` (`imei`) ON DELETE CASCADE" },
{ column = "profile_id", references = "`sky_phone_picstagram_profiles` (`id`) ON DELETE CASCADE" },
},
tableOptions = "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
},
{
name = "sky_phone_picstagram_posts",
columns = {
{ name = "id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
{ name = "profile_id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
{ name = "caption", type = "VARCHAR(800) NOT NULL DEFAULT ''" },
{ name = "location", type = "VARCHAR(80) NOT NULL DEFAULT ''" },
{ name = "comments_enabled", type = "TINYINT(1) NOT NULL DEFAULT 1" },
{ name = "status", type = "ENUM('published', 'archived', 'hidden', 'removed') NOT NULL DEFAULT 'published'" },
{ name = "created_at", type = "DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP" },
{ name = "updated_at", type = "DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" },
},
primaryKey = "id",
indexes = {
{ name = "idx_sky_phone_picstagram_feed", columns = "(`status`, `created_at`, `id`)" },
{ name = "idx_sky_phone_picstagram_profile_posts", columns = "(`profile_id`, `status`, `created_at`)" },
},
foreignKeys = {
{ column = "profile_id", references = "`sky_phone_picstagram_profiles` (`id`) ON DELETE CASCADE" },
},
tableOptions = "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
},
{
name = "sky_phone_picstagram_post_media",
columns = {
{ name = "id", type = "BIGINT UNSIGNED NOT NULL AUTO_INCREMENT" },
{ name = "post_id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
{ name = "media_id", type = "BIGINT UNSIGNED NOT NULL" },
{ name = "position", type = "TINYINT UNSIGNED NOT NULL" },
},
primaryKey = "id",
uniqueKeys = {
{ name = "uniq_sky_phone_picstagram_post_position", columns = "(`post_id`, `position`)" },
{ name = "uniq_sky_phone_picstagram_post_media", columns = "(`post_id`, `media_id`)" },
},
foreignKeys = {
{ column = "post_id", references = "`sky_phone_picstagram_posts` (`id`) ON DELETE CASCADE" },
{ column = "media_id", references = "`sky_phone_media` (`id`) ON DELETE CASCADE" },
},
tableOptions = "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
},
{
name = "sky_phone_picstagram_reactions",
columns = {
{ name = "id", type = "BIGINT UNSIGNED NOT NULL AUTO_INCREMENT" },
{ name = "post_id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
{ name = "profile_id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
{ name = "kind", type = "ENUM('like', 'save') NOT NULL" },
{ name = "created_at", type = "DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP" },
},
primaryKey = "id",
uniqueKeys = {
{ name = "uniq_sky_phone_picstagram_reaction", columns = "(`post_id`, `profile_id`, `kind`)" },
},
indexes = {
{ name = "idx_sky_phone_picstagram_saved", columns = "(`profile_id`, `kind`, `created_at`)" },
},
foreignKeys = {
{ column = "post_id", references = "`sky_phone_picstagram_posts` (`id`) ON DELETE CASCADE" },
{ column = "profile_id", references = "`sky_phone_picstagram_profiles` (`id`) ON DELETE CASCADE" },
},
tableOptions = "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
},
{
name = "sky_phone_picstagram_follows",
columns = {
{ name = "id", type = "BIGINT UNSIGNED NOT NULL AUTO_INCREMENT" },
{ name = "follower_id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
{ name = "following_id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
{ name = "status", type = "ENUM('pending', 'accepted') NOT NULL" },
{ name = "created_at", type = "DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP" },
{ name = "updated_at", type = "DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" },
},
primaryKey = "id",
uniqueKeys = {
{ name = "uniq_sky_phone_picstagram_follow", columns = "(`follower_id`, `following_id`)" },
},
indexes = {
{ name = "idx_sky_phone_picstagram_following", columns = "(`following_id`, `status`, `created_at`)" },
},
foreignKeys = {
{ column = "follower_id", references = "`sky_phone_picstagram_profiles` (`id`) ON DELETE CASCADE" },
{ column = "following_id", references = "`sky_phone_picstagram_profiles` (`id`) ON DELETE CASCADE" },
},
tableOptions = "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
},
{
name = "sky_phone_picstagram_comments",
columns = {
{ name = "id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
{ name = "post_id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
{ name = "profile_id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
{ name = "parent_id", type = "CHAR(36) NULL", characterSet = "ascii", collation = "ascii_bin" },
{ name = "body", type = "VARCHAR(300) NOT NULL" },
{ name = "status", type = "ENUM('visible', 'removed') NOT NULL DEFAULT 'visible'" },
{ name = "created_at", type = "DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP" },
},
primaryKey = "id",
indexes = {
{ name = "idx_sky_phone_picstagram_comments", columns = "(`post_id`, `status`, `created_at`)" },
},
foreignKeys = {
{ column = "post_id", references = "`sky_phone_picstagram_posts` (`id`) ON DELETE CASCADE" },
{ column = "profile_id", references = "`sky_phone_picstagram_profiles` (`id`) ON DELETE CASCADE" },
{ column = "parent_id", references = "`sky_phone_picstagram_comments` (`id`) ON DELETE CASCADE" },
},
tableOptions = "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
},
{
name = "sky_phone_picstagram_stories",
columns = {
{ name = "id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
{ name = "profile_id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
{ name = "media_id", type = "BIGINT UNSIGNED NOT NULL" },
{ name = "body", type = "VARCHAR(160) NOT NULL DEFAULT ''" },
{ name = "status", type = "ENUM('active', 'removed') NOT NULL DEFAULT 'active'" },
{ name = "expires_at", type = "DATETIME NOT NULL" },
{ name = "created_at", type = "DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP" },
},
primaryKey = "id",
indexes = {
{ name = "idx_sky_phone_picstagram_stories", columns = "(`profile_id`, `status`, `expires_at`)" },
{ name = "idx_sky_phone_picstagram_story_expiry", columns = "(`status`, `expires_at`)" },
},
foreignKeys = {
{ column = "profile_id", references = "`sky_phone_picstagram_profiles` (`id`) ON DELETE CASCADE" },
{ column = "media_id", references = "`sky_phone_media` (`id`) ON DELETE CASCADE" },
},
tableOptions = "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
},
{
name = "sky_phone_picstagram_story_views",
columns = {
{ name = "id", type = "BIGINT UNSIGNED NOT NULL AUTO_INCREMENT" },
{ name = "story_id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
{ name = "profile_id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
{ name = "created_at", type = "DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP" },
},
primaryKey = "id",
uniqueKeys = {
{ name = "uniq_sky_phone_picstagram_story_view", columns = "(`story_id`, `profile_id`)" },
},
foreignKeys = {
{ column = "story_id", references = "`sky_phone_picstagram_stories` (`id`) ON DELETE CASCADE" },
{ column = "profile_id", references = "`sky_phone_picstagram_profiles` (`id`) ON DELETE CASCADE" },
},
tableOptions = "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
},
{
name = "sky_phone_picstagram_activities",
columns = {
{ name = "id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
{ name = "recipient_id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
{ name = "actor_id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
{ name = "post_id", type = "CHAR(36) NULL", characterSet = "ascii", collation = "ascii_bin" },
{ name = "kind", type = "ENUM('follow_request', 'follow', 'request_accepted', 'like', 'comment', 'verified') NOT NULL" },
{ name = "read_at", type = "DATETIME NULL" },
{ name = "created_at", type = "DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP" },
},
primaryKey = "id",
indexes = {
{ name = "idx_sky_phone_picstagram_activity", columns = "(`recipient_id`, `read_at`, `created_at`)" },
},
foreignKeys = {
{ column = "recipient_id", references = "`sky_phone_picstagram_profiles` (`id`) ON DELETE CASCADE" },
{ column = "actor_id", references = "`sky_phone_picstagram_profiles` (`id`) ON DELETE CASCADE" },
{ column = "post_id", references = "`sky_phone_picstagram_posts` (`id`) ON DELETE SET NULL" },
},
tableOptions = "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
},
{
name = "sky_phone_picstagram_reports",
columns = {
{ name = "id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
{ name = "reporter_id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
{ name = "target_type", type = "ENUM('profile', 'post', 'story', 'comment') NOT NULL" },
{ name = "target_id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
{ name = "reason", type = "ENUM('spam', 'harassment', 'dangerous', 'illegal', 'other') NOT NULL" },
{ name = "details", type = "VARCHAR(500) NOT NULL DEFAULT ''" },
{ name = "status", type = "ENUM('open', 'reviewed', 'dismissed') NOT NULL DEFAULT 'open'" },
{ name = "resolved_action", type = "VARCHAR(16) NULL", characterSet = "ascii", collation = "ascii_general_ci" },
{ name = "resolved_at", type = "DATETIME NULL" },
{ name = "created_at", type = "DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP" },
},
primaryKey = "id",
uniqueKeys = {
{ name = "uniq_sky_phone_picstagram_report", columns = "(`reporter_id`, `target_type`, `target_id`)" },
},
indexes = {
{ name = "idx_sky_phone_picstagram_reports", columns = "(`status`, `created_at`)" },
},
foreignKeys = {
{ column = "reporter_id", references = "`sky_phone_picstagram_profiles` (`id`) ON DELETE CASCADE" },
},
tableOptions = "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
},
{
name = "sky_phone_picstagram_blocks",
columns = {
{ name = "id", type = "BIGINT UNSIGNED NOT NULL AUTO_INCREMENT" },
{ name = "blocker_id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
{ name = "blocked_id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
{ name = "created_at", type = "DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP" },
},
primaryKey = "id",
uniqueKeys = {
{ name = "uniq_sky_phone_picstagram_block", columns = "(`blocker_id`, `blocked_id`)" },
},
foreignKeys = {
{ column = "blocker_id", references = "`sky_phone_picstagram_profiles` (`id`) ON DELETE CASCADE" },
{ column = "blocked_id", references = "`sky_phone_picstagram_profiles` (`id`) ON DELETE CASCADE" },
},
tableOptions = "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
},
{
name = "sky_phone_picstagram_moderation_audit",
columns = {
{ name = "id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
{ name = "report_id", type = "CHAR(36) NULL", characterSet = "ascii", collation = "ascii_bin" },
{ name = "moderator_identifier", type = "VARCHAR(80) NOT NULL" },
{ name = "action", type = "VARCHAR(16) NOT NULL", characterSet = "ascii", collation = "ascii_general_ci" },
{ name = "target_type", type = "VARCHAR(16) NOT NULL", characterSet = "ascii", collation = "ascii_general_ci" },
{ name = "target_id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
{ name = "created_at", type = "DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP" },
},
primaryKey = "id",
indexes = {
{ name = "idx_sky_phone_picstagram_audit_target", columns = "(`target_type`, `target_id`, `created_at`)" },
},
foreignKeys = {
{ column = "report_id", references = "`sky_phone_picstagram_reports` (`id`) ON DELETE SET NULL" },
},
tableOptions = "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
},
{
name = "sky_phone_fliptok_profiles",
columns = {
File diff suppressed because it is too large Load Diff
+191
View File
@@ -423,3 +423,194 @@ CREATE TABLE IF NOT EXISTS `sky_phone_flare_messages` (
FOREIGN KEY (`match_id`) REFERENCES `sky_phone_flare_matches` (`id`) ON DELETE CASCADE,
FOREIGN KEY (`sender_account_id`) REFERENCES `sky_phone_accounts` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `sky_phone_picstagram_profiles` (
`id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`account_id` BIGINT UNSIGNED NOT NULL,
`handle` VARCHAR(24) CHARACTER SET ascii COLLATE ascii_general_ci NOT NULL,
`display_name` VARCHAR(40) NOT NULL,
`bio` VARCHAR(160) NOT NULL DEFAULT '',
`avatar_media_id` BIGINT UNSIGNED NULL,
`private` TINYINT(1) NOT NULL DEFAULT 0,
`verified` TINYINT(1) NOT NULL DEFAULT 0,
`status` ENUM('active','hidden','removed') NOT NULL DEFAULT 'active',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_sky_phone_picstagram_account` (`account_id`),
UNIQUE KEY `uniq_sky_phone_picstagram_handle` (`handle`),
FOREIGN KEY (`account_id`) REFERENCES `sky_phone_accounts` (`id`) ON DELETE CASCADE,
FOREIGN KEY (`avatar_media_id`) REFERENCES `sky_phone_media` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `sky_phone_picstagram_credentials` (
`profile_id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`password_hash` BINARY(32) NOT NULL,
`password_salt` CHAR(32) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`profile_id`),
FOREIGN KEY (`profile_id`) REFERENCES `sky_phone_picstagram_profiles` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `sky_phone_picstagram_sessions` (
`device_imei` CHAR(15) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`profile_id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`device_imei`),
KEY `idx_sky_phone_picstagram_sessions_profile` (`profile_id`,`updated_at`),
FOREIGN KEY (`device_imei`) REFERENCES `sky_phone_devices` (`imei`) ON DELETE CASCADE,
FOREIGN KEY (`profile_id`) REFERENCES `sky_phone_picstagram_profiles` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `sky_phone_picstagram_posts` (
`id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`profile_id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`caption` VARCHAR(800) NOT NULL DEFAULT '',
`location` VARCHAR(80) NOT NULL DEFAULT '',
`comments_enabled` TINYINT(1) NOT NULL DEFAULT 1,
`status` ENUM('published','archived','hidden','removed') NOT NULL DEFAULT 'published',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_sky_phone_picstagram_feed` (`status`,`created_at`,`id`),
KEY `idx_sky_phone_picstagram_profile_posts` (`profile_id`,`status`,`created_at`),
FOREIGN KEY (`profile_id`) REFERENCES `sky_phone_picstagram_profiles` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `sky_phone_picstagram_post_media` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`post_id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`media_id` BIGINT UNSIGNED NOT NULL,
`position` TINYINT UNSIGNED NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_sky_phone_picstagram_post_position` (`post_id`,`position`),
UNIQUE KEY `uniq_sky_phone_picstagram_post_media` (`post_id`,`media_id`),
FOREIGN KEY (`post_id`) REFERENCES `sky_phone_picstagram_posts` (`id`) ON DELETE CASCADE,
FOREIGN KEY (`media_id`) REFERENCES `sky_phone_media` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `sky_phone_picstagram_reactions` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`post_id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`profile_id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`kind` ENUM('like','save') NOT NULL,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_sky_phone_picstagram_reaction` (`post_id`,`profile_id`,`kind`),
KEY `idx_sky_phone_picstagram_saved` (`profile_id`,`kind`,`created_at`),
FOREIGN KEY (`post_id`) REFERENCES `sky_phone_picstagram_posts` (`id`) ON DELETE CASCADE,
FOREIGN KEY (`profile_id`) REFERENCES `sky_phone_picstagram_profiles` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `sky_phone_picstagram_follows` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`follower_id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`following_id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`status` ENUM('pending','accepted') NOT NULL,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_sky_phone_picstagram_follow` (`follower_id`,`following_id`),
KEY `idx_sky_phone_picstagram_following` (`following_id`,`status`,`created_at`),
FOREIGN KEY (`follower_id`) REFERENCES `sky_phone_picstagram_profiles` (`id`) ON DELETE CASCADE,
FOREIGN KEY (`following_id`) REFERENCES `sky_phone_picstagram_profiles` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `sky_phone_picstagram_comments` (
`id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`post_id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`profile_id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`parent_id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NULL,
`body` VARCHAR(300) NOT NULL,
`status` ENUM('visible','removed') NOT NULL DEFAULT 'visible',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_sky_phone_picstagram_comments` (`post_id`,`status`,`created_at`),
FOREIGN KEY (`post_id`) REFERENCES `sky_phone_picstagram_posts` (`id`) ON DELETE CASCADE,
FOREIGN KEY (`profile_id`) REFERENCES `sky_phone_picstagram_profiles` (`id`) ON DELETE CASCADE,
FOREIGN KEY (`parent_id`) REFERENCES `sky_phone_picstagram_comments` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `sky_phone_picstagram_stories` (
`id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`profile_id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`media_id` BIGINT UNSIGNED NOT NULL,
`body` VARCHAR(160) NOT NULL DEFAULT '',
`status` ENUM('active','removed') NOT NULL DEFAULT 'active',
`expires_at` DATETIME NOT NULL,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_sky_phone_picstagram_stories` (`profile_id`,`status`,`expires_at`),
KEY `idx_sky_phone_picstagram_story_expiry` (`status`,`expires_at`),
FOREIGN KEY (`profile_id`) REFERENCES `sky_phone_picstagram_profiles` (`id`) ON DELETE CASCADE,
FOREIGN KEY (`media_id`) REFERENCES `sky_phone_media` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `sky_phone_picstagram_story_views` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`story_id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`profile_id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_sky_phone_picstagram_story_view` (`story_id`,`profile_id`),
FOREIGN KEY (`story_id`) REFERENCES `sky_phone_picstagram_stories` (`id`) ON DELETE CASCADE,
FOREIGN KEY (`profile_id`) REFERENCES `sky_phone_picstagram_profiles` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `sky_phone_picstagram_activities` (
`id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`recipient_id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`actor_id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`post_id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NULL,
`kind` ENUM('follow_request','follow','request_accepted','like','comment','verified') NOT NULL,
`read_at` DATETIME NULL,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_sky_phone_picstagram_activity` (`recipient_id`,`read_at`,`created_at`),
FOREIGN KEY (`recipient_id`) REFERENCES `sky_phone_picstagram_profiles` (`id`) ON DELETE CASCADE,
FOREIGN KEY (`actor_id`) REFERENCES `sky_phone_picstagram_profiles` (`id`) ON DELETE CASCADE,
FOREIGN KEY (`post_id`) REFERENCES `sky_phone_picstagram_posts` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `sky_phone_picstagram_reports` (
`id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`reporter_id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`target_type` ENUM('profile','post','story','comment') NOT NULL,
`target_id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`reason` ENUM('spam','harassment','dangerous','illegal','other') NOT NULL,
`details` VARCHAR(500) NOT NULL DEFAULT '',
`status` ENUM('open','reviewed','dismissed') NOT NULL DEFAULT 'open',
`resolved_action` VARCHAR(16) CHARACTER SET ascii COLLATE ascii_general_ci NULL,
`resolved_at` DATETIME NULL,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_sky_phone_picstagram_report` (`reporter_id`,`target_type`,`target_id`),
KEY `idx_sky_phone_picstagram_reports` (`status`,`created_at`),
FOREIGN KEY (`reporter_id`) REFERENCES `sky_phone_picstagram_profiles` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `sky_phone_picstagram_blocks` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`blocker_id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`blocked_id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_sky_phone_picstagram_block` (`blocker_id`,`blocked_id`),
FOREIGN KEY (`blocker_id`) REFERENCES `sky_phone_picstagram_profiles` (`id`) ON DELETE CASCADE,
FOREIGN KEY (`blocked_id`) REFERENCES `sky_phone_picstagram_profiles` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `sky_phone_picstagram_moderation_audit` (
`id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`report_id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NULL,
`moderator_identifier` VARCHAR(80) NOT NULL,
`action` VARCHAR(16) CHARACTER SET ascii COLLATE ascii_general_ci NOT NULL,
`target_type` VARCHAR(16) CHARACTER SET ascii COLLATE ascii_general_ci NOT NULL,
`target_id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_sky_phone_picstagram_audit_target` (`target_type`,`target_id`,`created_at`),
FOREIGN KEY (`report_id`) REFERENCES `sky_phone_picstagram_reports` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;