ADD - implement Picstagram app

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

After

Width:  |  Height:  |  Size: 32 KiB

+6 -5
View File
@@ -124,11 +124,12 @@ describe('app registry', () => {
PHONE_APPS.filter((app) => app.category === 'social').map(
(app) => app.id,
),
).toEqual([
'fliptok',
'flare',
'radio',
'local-pages',
).toEqual([
'picstagram',
'fliptok',
'flare',
'radio',
'local-pages',
'phone',
'darkchat',
'banking',
+15
View File
@@ -57,6 +57,7 @@ import citymarktIcon from '@/assets/img/app-icons/citymarkt.webp'
import localPagesIcon from '@/assets/img/app-icons/local-pages.webp'
import flareIcon from '@/assets/img/app-icons/flare.svg'
import flipTokIcon from '@/assets/img/app-icons/fliptok.webp'
import picstagramIcon from '@/assets/img/app-icons/picstagram.webp'
import type {
LaunchablePhoneAppDefinition,
LaunchablePhoneAppId,
@@ -64,6 +65,20 @@ import type {
} from '@/types/apps'
export const PHONE_APPS: PhoneAppDefinition[] = [
{
category: 'social',
component: markRaw(
defineAsyncComponent(() => import('@/views/apps/PicstagramApp.vue')),
),
dockOrder: null,
gridOrder: 23,
icon: markRaw(Camera),
iconClass: 'app-icon--picstagram',
iconImage: picstagramIcon,
id: 'picstagram',
labelKey: 'Apps.picstagram.name',
route: '/apps/picstagram',
},
{
category: 'social',
component: markRaw(
+198
View File
@@ -191,6 +191,204 @@ const defaultLocales: LocaleTree = {
default: 'Flare could not complete the request.',
},
},
picstagram: {
name: 'Picstagram',
loading: 'Loading Picstagram',
home: 'Home',
explore: 'Explore',
create: 'Create',
activity: 'Activity',
profile: 'Profile',
following: 'Following',
city: 'City',
posts: 'Posts',
followers: 'Followers',
verified: 'Verified profile',
editProfile: 'Edit profile',
saved: 'Saved',
photos: 'Photos',
emptyBio: 'No bio yet.',
authTitle: 'Your Picstagram profile',
login: 'Sign In',
register: 'Register',
createAccount: 'Create Profile',
logout: 'Sign Out',
signingOut: 'Signing Out...',
loginBody:
'Sign in to continue with your photos, stories, follows, and saved posts.',
registerBody: 'Create a private Picstagram login for your photo profile.',
registrationHint:
'An iFruit account is required once to create and own a Picstagram profile.',
displayName: 'Name',
username: 'Username',
password: 'Password',
confirmPassword: 'Confirm password',
passwordsMismatch: 'The passwords do not match.',
displayNamePlaceholder: 'Your name',
usernamePlaceholder: 'username',
passwordPlaceholder: 'At least 8 characters',
confirmPasswordPlaceholder: 'Enter password again',
signOutTitle: 'Sign out of Picstagram?',
signOutBody:
'Your profile and posts stay online. This phone returns to the sign-in screen.',
stories: 'Stories',
yourStory: 'Your story',
noStories: 'No active stories',
storyViews: '{count} views',
storyViewers: 'Story viewers',
seenBy: 'Seen by {count}',
nextStory: 'Next story',
closeStory: 'Close story',
removeStory: 'Remove story',
storyRemoved: 'Story removed.',
emptyFeed: 'Your feed is quiet',
emptyFeedBody: 'Discover profiles or share the first photo.',
discoverPeople: 'Discover people',
searchPlaceholder: 'Search profiles, captions, places, or tags',
noResults: 'No results',
noResultsBody: 'Try another username, caption, place, or hashtag.',
newPost: 'New Post',
newStory: 'New Story',
post: 'Post',
story: 'Story',
choosePhotos: 'Choose photos',
choosePhoto: 'Choose photo',
choosePhotosHint: 'Select up to five photos from Gallery',
chooseStoryHint: 'Select one photo from Gallery',
selectedPhotos: '{count} selected',
changePhotos: 'Change selection',
caption: 'Caption',
captionPlaceholder: 'Write a caption...',
storyTextPlaceholder: 'Add a short story text...',
location: 'Location',
locationPlaceholder: 'Add a place',
allowComments: 'Allow comments',
publishing: 'Publishing...',
published: 'Your post is live.',
storyPublished: 'Your story is live.',
comments: 'Comments',
comment: 'Comment',
noComments: 'No comments yet',
addComment: 'Add a comment...',
reply: 'Reply',
removeComment: 'Remove comment',
likes: '{count} likes',
viewComments: 'View all {count} comments',
like: 'Like',
unlike: 'Unlike',
save: 'Save',
unsave: 'Remove from saved',
more: 'More',
archive: 'Archive',
restore: 'Restore',
deletePost: 'Delete post',
deletePostTitle: 'Delete this post?',
deletePostBody: 'The post and its reactions will no longer be available.',
privateProfile: 'This profile is private',
privateProfileBody: 'Follow this profile to see its posts and stories.',
follow: 'Follow',
requested: 'Requested',
unfollow: 'Following',
accept: 'Accept',
decline: 'Decline',
followRequests: 'Follow requests',
noActivity: 'No activity yet',
markRead: 'Mark as read',
publicProfile: 'Public profile',
privateProfileSetting: 'Private profile',
privacyHint: 'Only accepted followers can see your posts and stories.',
bio: 'Bio',
avatar: 'Profile photo',
chooseAvatar: 'Choose profile photo',
removeAvatar: 'Remove profile photo',
saveProfile: 'Save profile',
profileSaved: 'Profile updated.',
cancel: 'Cancel',
done: 'Done',
report: 'Report',
reportTarget: 'Report {target}',
reportReason: 'Reason',
reportDetails: 'Additional details (optional)',
submitReport: 'Submit report',
reported: 'Report submitted.',
block: 'Block profile',
unblock: 'Unblock profile',
blocked: 'Profile blocked.',
blockTitle: 'Block @{handle}?',
blockBody:
"You will no longer see or interact with each other's Picstagram content.",
moderation: 'Moderation',
reports: 'Open reports',
noReports: 'No open reports',
noDetails: 'No additional details',
hide: 'Hide',
remove: 'Remove',
restoreAction: 'Restore',
dismiss: 'Dismiss',
reportTargets: {
post: 'Post',
profile: 'Profile',
comment: 'Comment',
story: 'Story',
},
reportReasons: {
spam: 'Spam or misleading',
harassment: 'Harassment or bullying',
dangerous: 'Dangerous activity',
illegal: 'Illegal content',
other: 'Something else',
},
activityKinds: {
follow_request: 'requested to follow you',
follow: 'started following you',
request_accepted: 'accepted your follow request',
like: 'liked your post',
comment: 'commented on your post',
verified: 'verification changed',
},
notifications: {
follow_request: '{actor} requested to follow you.',
follow: '{actor} started following you.',
request_accepted: '{actor} accepted your follow request.',
like: '{actor} liked your post.',
comment: '{actor} commented on your post.',
verified: 'Your Picstagram profile is now verified.',
default: 'You have new Picstagram activity.',
},
errors: {
invalid_post: 'Check the post details.',
invalid_story: 'Check the story details.',
invalid_media: 'Choose photos owned by this phone.',
invalid_comment: 'Enter a valid comment.',
comments_disabled: 'Comments are disabled.',
invalid_profile: 'Check your profile details.',
invalid_handle: 'Use 324 letters, numbers, dots, or underscores.',
invalid_display_name: 'Enter a display name.',
invalid_password: 'Password must be 872 characters.',
invalid_credentials: 'Username or password is incorrect.',
already_registered:
'This iFruit account already owns a Picstagram profile.',
handle_taken: 'This username is already taken.',
post_not_found: 'This post is unavailable.',
story_not_found: 'This story is unavailable.',
profile_not_found: 'This profile is unavailable.',
request_not_found: 'This follow request is no longer open.',
profile_unavailable: 'This profile is unavailable.',
blocked: 'This profile is blocked.',
already_reported: 'You already reported this content.',
invalid_report: 'Choose a valid report reason.',
invalid_search: 'Enter a valid search.',
invalid_cursor: 'The feed changed. Refresh and try again.',
invalid_status: 'This action is not available.',
not_authorized: 'You are not allowed to do that.',
report_not_found: 'This report is no longer open.',
rate_limited: 'Too many actions. Try again shortly.',
not_authenticated: 'Sign in to iFruit first.',
picstagram_not_authenticated: 'Sign in to Picstagram first.',
request_failed: 'The request failed.',
default: 'Picstagram could not complete the request.',
},
},
fliptok: {
name: 'FlipTok',
loading: 'Loading FlipTok',
+216
View File
@@ -0,0 +1,216 @@
import { createPinia, setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { usePicstagramStore } from '@/stores/picstagram'
import type {
PicstagramActivity,
PicstagramComment,
PicstagramPost,
PicstagramProfile,
PicstagramStory,
} from '@/types/picstagram'
import { nuiCall } from '@/utils/nui'
vi.mock('@/utils/nui', () => ({
nuiCall: vi.fn(),
}))
const profile: PicstagramProfile = {
avatar_media_id: null,
avatar_url: null,
bio: 'City light collector.',
display_name: 'Nova',
follow_status: null,
followers: 12,
following: 4,
handle: 'nova',
id: 'profile-1',
is_following: false,
is_owner: true,
is_requested: false,
locked: false,
post_count: 1,
private: false,
status: 'active',
verified: false,
}
const post: PicstagramPost = {
avatar_url: null,
caption: 'Los Santos after rain.',
comment_count: 1,
comments_enabled: true,
created_at: 1,
display_name: 'Nova',
handle: 'nova',
id: 'post-1',
is_liked: false,
is_owner: true,
is_saved: false,
like_count: 2,
location: 'Downtown',
media: [{ id: 1, position: 0, url: 'https://example.com/photo.webp' }],
private: false,
profile_id: profile.id,
verified: false,
}
const comment: PicstagramComment = {
avatar_url: null,
body: 'Beautiful.',
created_at: 1,
display_name: 'Nova',
handle: 'nova',
id: 'comment-1',
is_owner: true,
parent_id: null,
profile_id: profile.id,
verified: false,
}
const activity: PicstagramActivity = {
avatar_url: null,
created_at: 1,
display_name: 'Nova',
handle: 'nova',
id: 'activity-1',
kind: 'follow',
post_id: null,
post_url: null,
profile_id: profile.id,
read_at: null,
verified: false,
}
const story: PicstagramStory = {
avatar_url: null,
body: 'Tonight.',
created_at: 1,
display_name: 'Nova',
expires_at: 2,
handle: 'nova',
id: 'story-1',
is_owner: true,
profile_id: profile.id,
seen: true,
url: 'https://example.com/story.webp',
verified: false,
view_count: 3,
}
describe('Picstagram store', () => {
beforeEach(() => {
setActivePinia(createPinia())
vi.mocked(nuiCall).mockReset()
})
it('updates verification on every visible surface', () => {
const store = usePicstagramStore()
store.profile = { ...profile }
store.feed = [{ ...post }]
store.explore = [{ ...post }]
store.profilePosts = [{ ...post }]
store.saved = [{ ...post }]
store.searchPosts = [{ ...post }]
store.comments = [{ ...comment }]
store.activities = [{ ...activity }]
store.applyVerification(profile.id, true)
expect(store.profile.verified).toBe(true)
expect(
store
.allPostCollections()
.flat()
.every((item) => item.verified),
).toBe(true)
expect(store.comments[0].verified).toBe(true)
expect(store.activities[0].verified).toBe(true)
})
it('rolls back an optimistic like when the server rejects it', async () => {
vi.mocked(nuiCall).mockResolvedValue({ success: false })
const store = usePicstagramStore()
store.feed = [{ ...post }]
store.explore = [{ ...post }]
expect(await store.react(store.feed[0], 'like')).toBe(false)
expect(store.feed[0].is_liked).toBe(false)
expect(store.feed[0].like_count).toBe(2)
expect(store.explore[0].is_liked).toBe(false)
expect(store.explore[0].like_count).toBe(2)
})
it('removes a blocked profile from all local surfaces', async () => {
vi.mocked(nuiCall).mockResolvedValue({ success: true })
const store = usePicstagramStore()
store.feed = [{ ...post }]
store.explore = [{ ...post }]
store.profilePosts = [{ ...post }]
store.saved = [{ ...post }]
store.searchPosts = [{ ...post }]
store.comments = [{ ...comment }]
store.activities = [{ ...activity }]
store.stories = [{ ...story }]
store.viewedProfile = { ...profile, is_owner: false }
expect(await store.blockProfile(profile.id)).toBe(true)
expect(store.allPostCollections().flat()).toEqual([])
expect(store.comments).toEqual([])
expect(store.activities).toEqual([])
expect(store.stories).toEqual([])
expect(store.viewedProfile).toBeNull()
})
it('keeps the app signed out when no Picstagram session exists', async () => {
vi.mocked(nuiCall).mockResolvedValue({
success: true,
data: { authenticated: false, isAdmin: false },
})
const store = usePicstagramStore()
expect(await store.bootstrap()).toBe(true)
expect(store.authenticated).toBe(false)
expect(store.profile).toBeNull()
expect(store.feed).toEqual([])
})
it('hydrates the profile and stories after login', async () => {
vi.mocked(nuiCall)
.mockResolvedValueOnce({ success: true })
.mockResolvedValueOnce({
success: true,
data: {
authenticated: true,
feed: { hasMore: false, items: [{ ...post }], nextCursor: null },
isAdmin: false,
profile: { ...profile },
},
})
.mockResolvedValueOnce({ success: true, data: [{ ...story }] })
const store = usePicstagramStore()
expect((await store.login('nova', 'password123')).success).toBe(true)
expect(store.profile?.handle).toBe('nova')
expect(store.stories).toHaveLength(1)
expect(nuiCall).toHaveBeenNthCalledWith(1, 'picstagram:login', {
handle: 'nova',
password: 'password123',
})
})
it('clears all local state after logout', async () => {
vi.mocked(nuiCall).mockResolvedValue({ success: true })
const store = usePicstagramStore()
store.authenticated = true
store.profile = { ...profile }
store.feed = [{ ...post }]
store.stories = [{ ...story }]
expect((await store.logout()).success).toBe(true)
expect(store.authenticated).toBe(false)
expect(store.profile).toBeNull()
expect(store.feed).toEqual([])
expect(store.stories).toEqual([])
})
})
+399
View File
@@ -0,0 +1,399 @@
import { defineStore } from 'pinia'
import type {
PicstagramActivity,
PicstagramComment,
PicstagramPage,
PicstagramPost,
PicstagramProfile,
PicstagramProfilePage,
PicstagramReport,
PicstagramReportReason,
PicstagramReportTarget,
PicstagramSearchResult,
PicstagramStory,
PicstagramStoryViewer,
} from '@/types/picstagram'
import { nuiCall, type NuiResponse } from '@/utils/nui'
function replaceProfileState(
posts: PicstagramPost[],
profileId: string,
update: (post: PicstagramPost) => void,
): void {
posts.filter((post) => post.profile_id === profileId).forEach(update)
}
export const usePicstagramStore = defineStore('picstagram', {
state: () => ({
activities: [] as PicstagramActivity[],
authenticated: false,
comments: [] as PicstagramComment[],
explore: [] as PicstagramPost[],
exploreCursor: null as string | null,
feed: [] as PicstagramPost[],
feedCursor: null as string | null,
isAdmin: false,
loading: false,
profile: null as PicstagramProfile | null,
profilePosts: [] as PicstagramPost[],
reports: [] as PicstagramReport[],
saved: [] as PicstagramPost[],
searchPosts: [] as PicstagramPost[],
searchProfiles: [] as PicstagramProfile[],
stories: [] as PicstagramStory[],
storyViewers: [] as PicstagramStoryViewer[],
viewedProfile: null as PicstagramProfile | null,
}),
actions: {
allPostCollections(): PicstagramPost[][] {
return [
this.feed,
this.explore,
this.profilePosts,
this.saved,
this.searchPosts,
]
},
applyVerification(profileId: string, verified: boolean): void {
if (this.profile?.id === profileId) this.profile.verified = verified
if (this.viewedProfile?.id === profileId)
this.viewedProfile.verified = verified
this.allPostCollections().forEach((posts) =>
replaceProfileState(posts, profileId, (post) => {
post.verified = verified
}),
)
this.comments
.filter((comment) => comment.profile_id === profileId)
.forEach((comment) => {
comment.verified = verified
})
this.activities
.filter((activity) => activity.profile_id === profileId)
.forEach((activity) => {
activity.verified = verified
})
},
async bootstrap(): Promise<boolean> {
this.loading = true
const response = await nuiCall<{
authenticated: boolean
feed?: PicstagramPage
isAdmin: boolean
profile?: PicstagramProfile
}>('picstagram:bootstrap')
this.loading = false
if (!response.success || !response.data) return false
this.authenticated = response.data.authenticated
this.isAdmin = response.data.isAdmin
this.profile = response.data.profile ?? null
this.feed = response.data.feed?.items ?? []
this.feedCursor = response.data.feed?.nextCursor ?? null
if (this.authenticated) await this.loadStories()
return true
},
async login(handle: string, password: string): Promise<NuiResponse> {
const response = await nuiCall('picstagram:login', { handle, password })
if (response.success) await this.bootstrap()
return response
},
async register(
displayName: string,
handle: string,
password: string,
): Promise<NuiResponse> {
const response = await nuiCall('picstagram:register', {
displayName,
handle,
password,
})
if (response.success) await this.bootstrap()
return response
},
async logout(): Promise<NuiResponse> {
const response = await nuiCall('picstagram:logout')
if (response.success) this.$reset()
return response
},
async loadFeed(append = false): Promise<boolean> {
const response = await nuiCall<PicstagramPage>('picstagram:feed', {
cursor: append ? this.feedCursor : undefined,
})
if (!response.success || !response.data) return false
this.feed = append
? [...this.feed, ...response.data.items]
: response.data.items
this.feedCursor = response.data.nextCursor
return true
},
async loadExplore(append = false): Promise<boolean> {
const response = await nuiCall<PicstagramPage>('picstagram:explore', {
cursor: append ? this.exploreCursor : undefined,
})
if (!response.success || !response.data) return false
this.explore = append
? [...this.explore, ...response.data.items]
: response.data.items
this.exploreCursor = response.data.nextCursor
return true
},
async search(search: string): Promise<boolean> {
if (!search.trim()) {
this.searchPosts = []
this.searchProfiles = []
return true
}
const response = await nuiCall<PicstagramSearchResult>(
'picstagram:search',
{ search },
)
if (!response.success || !response.data) return false
this.searchPosts = response.data.posts
this.searchProfiles = response.data.profiles
return true
},
async loadSaved(): Promise<boolean> {
const response = await nuiCall<PicstagramPage>('picstagram:saved')
if (!response.success || !response.data) return false
this.saved = response.data.items
return true
},
async loadProfile(query: {
handle?: string
profileId?: string
}): Promise<boolean> {
const response = await nuiCall<PicstagramProfilePage>(
'picstagram:profile',
query,
)
if (!response.success || !response.data) return false
this.viewedProfile = response.data.profile
this.profilePosts = response.data.posts.items
return true
},
showOwnProfile(): void {
this.viewedProfile = null
this.profilePosts = this.feed.filter((post) => post.is_owner)
},
async updateProfile(payload: {
avatarMediaId: number
bio: string
displayName: string
handle: string
private: boolean
}): Promise<NuiResponse<PicstagramProfile>> {
const response = await nuiCall<PicstagramProfile>(
'picstagram:update-profile',
payload,
)
if (response.success && response.data) this.profile = response.data
return response
},
async publishPost(payload: {
caption: string
commentsEnabled: boolean
location: string
mediaIds: number[]
}): Promise<NuiResponse<{ id: string }>> {
const response = await nuiCall<{ id: string }>(
'picstagram:publish-post',
payload,
)
if (response.success) await this.loadFeed()
return response
},
async setPostStatus(
post: PicstagramPost,
status: 'archived' | 'published' | 'removed',
): Promise<boolean> {
const response = await nuiCall('picstagram:set-post-status', {
id: post.id,
status,
})
if (!response.success) return false
this.allPostCollections().forEach((posts) => {
const index = posts.findIndex((item) => item.id === post.id)
if (index >= 0) posts.splice(index, 1)
})
return true
},
async react(post: PicstagramPost, kind: 'like' | 'save'): Promise<boolean> {
const key = kind === 'like' ? 'is_liked' : 'is_saved'
const next = !post[key]
const matching = this.allPostCollections()
.flat()
.filter((item) => item.id === post.id)
matching.forEach((item) => {
item[key] = next
if (kind === 'like') item.like_count += next ? 1 : -1
})
const response = await nuiCall('picstagram:react', {
active: next,
id: post.id,
kind,
})
if (response.success) return true
matching.forEach((item) => {
item[key] = !next
if (kind === 'like') item.like_count += next ? -1 : 1
})
return false
},
async followProfile(profile: PicstagramProfile): Promise<boolean> {
const active = !profile.is_following && !profile.is_requested
const response = await nuiCall<{
status: 'accepted' | 'pending' | false
}>('picstagram:follow', { active, profileId: profile.id })
if (!response.success || !response.data) return false
const previousFollowing = profile.is_following
profile.follow_status = response.data.status || null
profile.is_following = response.data.status === 'accepted'
profile.is_requested = response.data.status === 'pending'
if (previousFollowing !== profile.is_following)
profile.followers += profile.is_following ? 1 : -1
return true
},
async respondFollow(profileId: string, accept: boolean): Promise<boolean> {
const response = await nuiCall('picstagram:respond-follow', {
accept,
profileId,
})
if (response.success)
this.activities = this.activities.filter(
(activity) =>
activity.kind !== 'follow_request' ||
activity.profile_id !== profileId,
)
return response.success
},
async loadComments(id: string): Promise<boolean> {
const response = await nuiCall<PicstagramComment[]>(
'picstagram:comments',
{ id },
)
this.comments = response.success && response.data ? response.data : []
return response.success
},
async comment(
id: string,
body: string,
parentId?: string,
): Promise<NuiResponse> {
return nuiCall('picstagram:comment', { body, id, parentId })
},
async removeComment(id: string): Promise<boolean> {
const response = await nuiCall('picstagram:remove-comment', { id })
if (response.success)
this.comments = this.comments.filter((comment) => comment.id !== id)
return response.success
},
async loadStories(): Promise<boolean> {
const response = await nuiCall<PicstagramStory[]>('picstagram:stories')
this.stories = response.success && response.data ? response.data : []
return response.success
},
async publishStory(
mediaId: number,
body: string,
): Promise<NuiResponse<{ id: string }>> {
const response = await nuiCall<{ id: string }>(
'picstagram:publish-story',
{ body, mediaId },
)
if (response.success) await this.loadStories()
return response
},
async viewStory(story: PicstagramStory): Promise<void> {
if (!story.seen && !story.is_owner) {
const response = await nuiCall('picstagram:view-story', {
id: story.id,
})
if (response.success) story.seen = true
}
},
async loadStoryViewers(id: string): Promise<boolean> {
const response = await nuiCall<PicstagramStoryViewer[]>(
'picstagram:story-viewers',
{ id },
)
this.storyViewers = response.success && response.data ? response.data : []
return response.success
},
async removeStory(id: string): Promise<boolean> {
const response = await nuiCall('picstagram:remove-story', { id })
if (response.success)
this.stories = this.stories.filter((story) => story.id !== id)
return response.success
},
async loadActivities(): Promise<boolean> {
const response = await nuiCall<PicstagramActivity[]>(
'picstagram:activities',
)
this.activities = response.success && response.data ? response.data : []
return response.success
},
async markActivities(): Promise<boolean> {
const response = await nuiCall('picstagram:mark-activities')
if (response.success)
this.activities.forEach((activity) => {
activity.read_at ??= new Date().toISOString()
})
return response.success
},
async blockProfile(profileId: string): Promise<boolean> {
const response = await nuiCall('picstagram:block', {
active: true,
profileId,
})
if (!response.success) return false
this.allPostCollections().forEach((posts) => {
for (let index = posts.length - 1; index >= 0; index -= 1)
if (posts[index].profile_id === profileId) posts.splice(index, 1)
})
this.comments = this.comments.filter(
(comment) => comment.profile_id !== profileId,
)
this.activities = this.activities.filter(
(activity) => activity.profile_id !== profileId,
)
this.stories = this.stories.filter(
(story) => story.profile_id !== profileId,
)
if (this.viewedProfile?.id === profileId) this.viewedProfile = null
return true
},
async report(
targetType: PicstagramReportTarget,
targetId: string,
reason: PicstagramReportReason,
details: string,
): Promise<NuiResponse> {
return nuiCall('picstagram:report', {
details,
reason,
targetId,
targetType,
})
},
async loadReports(): Promise<boolean> {
const response = await nuiCall<PicstagramReport[]>(
'picstagram:admin-reports',
)
this.reports = response.success && response.data ? response.data : []
return response.success
},
async resolveReport(
id: string,
action: 'dismiss' | 'hide' | 'remove' | 'restore',
): Promise<boolean> {
const response = await nuiCall('picstagram:admin-resolve-report', {
action,
id,
})
if (response.success)
this.reports = this.reports.filter((report) => report.id !== id)
return response.success
},
},
})
+1
View File
@@ -29,6 +29,7 @@ export type PhoneAppId =
| 'local-pages'
| 'flare'
| 'fliptok'
| 'picstagram'
export type LaunchablePhoneAppId = PhoneAppId
+141
View File
@@ -0,0 +1,141 @@
export type PicstagramProfile = {
avatar_media_id: number | null
avatar_url: string | null
bio: string
display_name: string
follow_status: 'accepted' | 'pending' | null
followers: number
following: number
handle: string
id: string
is_following: boolean
is_owner: boolean
is_requested: boolean
locked: boolean
post_count: number
private: boolean
status: 'active'
verified: boolean
}
export type PicstagramPostMedia = {
id: number
position: number
url: string
}
export type PicstagramPost = {
avatar_url: string | null
caption: string
comment_count: number
comments_enabled: boolean
created_at: number
display_name: string
handle: string
id: string
is_liked: boolean
is_owner: boolean
is_saved: boolean
like_count: number
location: string
media: PicstagramPostMedia[]
private: boolean
profile_id: string
verified: boolean
}
export type PicstagramPage = {
hasMore: boolean
items: PicstagramPost[]
nextCursor: string | null
}
export type PicstagramProfilePage = {
posts: PicstagramPage
profile: PicstagramProfile
}
export type PicstagramSearchResult = {
posts: PicstagramPost[]
profiles: PicstagramProfile[]
}
export type PicstagramComment = {
avatar_url: string | null
body: string
created_at: number
display_name: string
handle: string
id: string
is_owner: boolean
parent_id: string | null
profile_id: string
verified: boolean
}
export type PicstagramStory = {
avatar_url: string | null
body: string
created_at: number
display_name: string
expires_at: number
handle: string
id: string
is_owner: boolean
profile_id: string
seen: boolean
url: string
verified: boolean
view_count: number
}
export type PicstagramStoryViewer = {
avatar_url: string | null
created_at: number
display_name: string
handle: string
id: string
verified: boolean
}
export type PicstagramActivityKind =
| 'follow_request'
| 'follow'
| 'request_accepted'
| 'like'
| 'comment'
| 'verified'
export type PicstagramActivity = {
avatar_url: string | null
created_at: number
display_name: string
handle: string
id: string
kind: PicstagramActivityKind
post_id: string | null
post_url: string | null
profile_id: string
read_at: string | null
verified: boolean
}
export type PicstagramReportTarget = 'profile' | 'post' | 'story' | 'comment'
export type PicstagramReportReason =
| 'spam'
| 'harassment'
| 'dangerous'
| 'illegal'
| 'other'
export type PicstagramReport = {
created_at: number
details: string
id: string
reason: PicstagramReportReason
reporter_display_name: string
reporter_handle: string
target_id: string
target_type: PicstagramReportTarget
}
+1
View File
@@ -82,6 +82,7 @@ const DEFAULT_APP_NOTIFICATIONS: Record<
'neon-drop': { enabled: true, sounds: true },
citymarkt: { enabled: true, sounds: true },
'local-pages': { enabled: true, sounds: true },
picstagram: { enabled: true, sounds: true },
fliptok: { enabled: true, sounds: true },
camera: { enabled: true, sounds: true },
clock: { enabled: true, sounds: true },
File diff suppressed because it is too large Load Diff