mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-08-29 01:01:31 +00:00
ADD - implement Picstagram app
This commit is contained in:
@@ -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 |
@@ -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',
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 3–24 letters, numbers, dots, or underscores.',
|
||||
invalid_display_name: 'Enter a display name.',
|
||||
invalid_password: 'Password must be 8–72 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',
|
||||
|
||||
@@ -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([])
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -29,6 +29,7 @@ export type PhoneAppId =
|
||||
| 'local-pages'
|
||||
| 'flare'
|
||||
| 'fliptok'
|
||||
| 'picstagram'
|
||||
|
||||
export type LaunchablePhoneAppId = PhoneAppId
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 3–24 letters, numbers, dots, or underscores.",
|
||||
invalid_display_name = "Enter a display name.", invalid_password = "Password must be 8–72 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",
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user