Compare commits

..

1 Commits

Author SHA1 Message Date
DerEchteAlec 4879d4569b FIX - resolve CodeQL security alerts 2026-08-20 17:23:21 +02:00
44 changed files with 111 additions and 18253 deletions
-5
View File
@@ -577,11 +577,6 @@ pnpm install
pnpm dev pnpm dev
``` ```
The browser mock links `demo@ifruit.com` and signs in the seeded SkyPic profile
`@alexm` by default. To exercise SkyPic registration with an empty profile, open
`http://localhost:5174/?testScenario=skypic-onboarding#/apps/skypic` while the
development server is running.
Create a production frontend build with: Create a production frontend build with:
```powershell ```powershell
-125
View File
@@ -45,7 +45,6 @@ import { useDarkChatStore } from '@/stores/darkchat'
import { useFlareStore } from '@/stores/flare' import { useFlareStore } from '@/stores/flare'
import { useFlipTokStore } from '@/stores/fliptok' import { useFlipTokStore } from '@/stores/fliptok'
import { usePicstagramStore } from '@/stores/picstagram' import { usePicstagramStore } from '@/stores/picstagram'
import { useSkyPicStore } from '@/stores/skypic'
import { useFeatherStore } from '@/stores/feather' import { useFeatherStore } from '@/stores/feather'
import { useMediaStore } from '@/stores/media' import { useMediaStore } from '@/stores/media'
import { useMarketplaceStore } from '@/stores/marketplace' import { useMarketplaceStore } from '@/stores/marketplace'
@@ -97,8 +96,6 @@ type AppMessage = {
| FlipTokNotificationData | FlipTokNotificationData
| PicstagramVerificationData | PicstagramVerificationData
| PicstagramNotificationData | PicstagramNotificationData
| SkyPicNotificationData
| SkyPicChangedData
| FeatherNotificationData | FeatherNotificationData
| BankingChangedData | BankingChangedData
| CryptoMarketChangedData | CryptoMarketChangedData
@@ -233,28 +230,6 @@ type PicstagramNotificationData = {
title?: string title?: string
} }
type SkyPicNotificationData = {
actor?: string
device?: PhoneNotificationDevicePayload
kind?:
| 'friend_request'
| 'friend_accepted'
| 'snap'
| 'message'
| 'story_reply'
| 'snap_opened'
profileId?: string
snapId?: string
text?: string
title?: string
}
type SkyPicChangedData = {
device?: PhoneNotificationDevicePayload
profileId?: string
reason?: 'account_deleted'
}
type FeatherNotificationData = { type FeatherNotificationData = {
actor?: string actor?: string
device?: PhoneNotificationDevicePayload device?: PhoneNotificationDevicePayload
@@ -323,7 +298,6 @@ const darkchat = useDarkChatStore()
const flare = useFlareStore() const flare = useFlareStore()
const fliptok = useFlipTokStore() const fliptok = useFlipTokStore()
const picstagram = usePicstagramStore() const picstagram = usePicstagramStore()
const skypic = useSkyPicStore()
const feather = useFeatherStore() const feather = useFeatherStore()
const media = useMediaStore() const media = useMediaStore()
const marketplace = useMarketplaceStore() const marketplace = useMarketplaceStore()
@@ -346,7 +320,6 @@ const WHITE_STATUS_BAR_APP_IDS = new Set([
'calculator', 'calculator',
'camera', 'camera',
'fliptok', 'fliptok',
'skypic',
'neon-drop', 'neon-drop',
'sky-flappy', 'sky-flappy',
'snake', 'snake',
@@ -531,35 +504,6 @@ function cancelUnlockedPhoneDataLoad(): void {
unlockedServicesIdle = undefined unlockedServicesIdle = undefined
} }
async function refreshSkyPicState(refreshThread = false): Promise<void> {
if (!account.email || !appAuth.isSignedIn('skypic')) {
if (!account.email) {
skypic.resetSession()
} else {
if (!skypic.bootstrapPending) skypic.resetSession()
}
return
}
if (skypic.accountDeletePending) return
const accountEmail = account.email
const imei = phone.device?.imei ?? ''
const loaded = await skypic.bootstrap()
if (
!loaded ||
account.email !== accountEmail ||
(phone.device?.imei ?? '') !== imei ||
!appAuth.isSignedIn('skypic')
) {
return
}
if (!skypic.profile) {
appAuth.signOut('skypic')
skypic.resetSession()
return
}
if (refreshThread) await skypic.refreshActiveThread()
}
function queueCompaniesChange(change: CompanyChangedPayload): void { function queueCompaniesChange(change: CompanyChangedPayload): void {
if (!pendingCompaniesChange) { if (!pendingCompaniesChange) {
pendingCompaniesChange = { ...change } pendingCompaniesChange = { ...change }
@@ -587,7 +531,6 @@ function queueCompaniesChange(change: CompanyChangedPayload): void {
async function bootstrapUnlockedPhoneData(): Promise<void> { async function bootstrapUnlockedPhoneData(): Promise<void> {
const tasks: Array<() => Promise<unknown> | void> = [ const tasks: Array<() => Promise<unknown> | void> = [
() => refreshSkyPicState(),
() => calls.bootstrap(), () => calls.bootstrap(),
() => messages.loadConversations(), () => messages.loadConversations(),
() => billing.loadOverview(), () => billing.loadOverview(),
@@ -727,19 +670,6 @@ function openDevelopmentPayphonePreview(): void {
) )
} }
function skyPicNotificationRoute(data: SkyPicNotificationData): string {
const query = new URLSearchParams()
query.set(
'tab',
data.kind === 'friend_request' || data.kind === 'friend_accepted'
? 'friends'
: 'chats',
)
if (data.profileId) query.set('profileId', data.profileId)
if (data.kind === 'snap' && data.snapId) query.set('snap', data.snapId)
return `/apps/skypic?${query.toString()}`
}
function onMessage(event: MessageEvent<AppMessage>): void { function onMessage(event: MessageEvent<AppMessage>): void {
if (!isTrustedRootMessageSource(event.source, window)) return if (!isTrustedRootMessageSource(event.source, window)) return
@@ -971,47 +901,6 @@ function onMessage(event: MessageEvent<AppMessage>): void {
} }
notifications.show(notification) notifications.show(notification)
if (phone.isOpen) void picstagram.loadActivities() if (phone.isOpen) void picstagram.loadActivities()
} else if (event.data?.type === 'skypic:new' && event.data.data) {
const data = event.data.data as SkyPicNotificationData
const targetsActiveDevice =
!data.device || data.device.imei === phone.device?.imei
const signedInOnActiveDevice = appAuth.isSignedIn('skypic')
const notification: PhoneNotificationInput = {
appId: 'skypic',
route: skyPicNotificationRoute(data),
subtitle: data.actor,
text: data.text ?? phone.t('Apps.skypic.notifications.default'),
title: data.title ?? phone.t('Apps.skypic.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),
}
}
if (!targetsActiveDevice || signedInOnActiveDevice) {
notifications.show(notification)
}
if (phone.isOpen && targetsActiveDevice && signedInOnActiveDevice) {
void refreshSkyPicState(true)
} else if (
targetsActiveDevice &&
!signedInOnActiveDevice &&
!skypic.bootstrapPending
) {
skypic.resetSession()
}
} else if (event.data?.type === 'skypic:changed' && event.data.data) {
const data = event.data.data as SkyPicChangedData
const targetsActiveDevice =
!data.device || data.device.imei === phone.device?.imei
if (phone.isOpen && targetsActiveDevice) {
void refreshSkyPicState(true)
}
} else if ( } else if (
event.data?.type === 'marketplace:new-message' && event.data?.type === 'marketplace:new-message' &&
event.data.data event.data.data
@@ -1647,7 +1536,6 @@ watch(
if (!isOpen) { if (!isOpen) {
updateTextInputFocus(false) updateTextInputFocus(false)
cancelUnlockedPhoneDataLoad() cancelUnlockedPhoneDataLoad()
skypic.resetSession()
appStore.cancelPendingInstalls() appStore.cancelPendingInstalls()
activitySuspended.value = false activitySuspended.value = false
weather.stop() weather.stop()
@@ -1696,19 +1584,6 @@ watch(
}, },
) )
watch(
() => [phone.device?.imei ?? '', account.email] as const,
([imei, email], [previousImei, previousEmail]) => {
if (imei === previousImei && email === previousEmail) return
skypic.resetSession()
cancelUnlockedPhoneDataLoad()
unlockedServicesLoaded.value = false
if (phone.isOpen && !isLocked.value && !setupRequired.value) {
loadUnlockedPhoneData()
}
},
)
watch( watch(
() => phone.cameraLandscape, () => phone.cameraLandscape,
(landscape) => { (landscape) => {
Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

-3
View File
@@ -10,7 +10,6 @@ import { useBillingStore } from '@/stores/billing'
import { useCompaniesStore } from '@/stores/companies' import { useCompaniesStore } from '@/stores/companies'
import { useMarketplaceStore } from '@/stores/marketplace' import { useMarketplaceStore } from '@/stores/marketplace'
import { useDarkChatStore } from '@/stores/darkchat' import { useDarkChatStore } from '@/stores/darkchat'
import { useSkyPicStore } from '@/stores/skypic'
import { usePhoneStore } from '@/stores/phone' import { usePhoneStore } from '@/stores/phone'
import type { PhoneAppDefinition } from '@/types/apps' import type { PhoneAppDefinition } from '@/types/apps'
import { import {
@@ -55,7 +54,6 @@ const billing = useBillingStore()
const companies = useCompaniesStore() const companies = useCompaniesStore()
const marketplace = useMarketplaceStore() const marketplace = useMarketplaceStore()
const darkchat = useDarkChatStore() const darkchat = useDarkChatStore()
const skypic = useSkyPicStore()
const router = useRouter() const router = useRouter()
const iconFailed = ref(false) const iconFailed = ref(false)
const isDragging = ref(false) const isDragging = ref(false)
@@ -104,7 +102,6 @@ const unreadCount = computed(() => {
if (props.app.id === 'darkchat') return darkchat.unreadCount if (props.app.id === 'darkchat') return darkchat.unreadCount
if (props.app.id === 'billing') return billing.overview?.unreadCount ?? 0 if (props.app.id === 'billing') return billing.overview?.unreadCount ?? 0
if (props.app.id === 'companies') return companies.unreadCount if (props.app.id === 'companies') return companies.unreadCount
if (props.app.id === 'skypic') return skypic.unreadCount
return 0 return 0
}) })
const calendarWeekday = computed(() => const calendarWeekday = computed(() =>
@@ -1,80 +0,0 @@
import { readFileSync } from 'node:fs'
import { createSSRApp, h } from 'vue'
import { renderToString } from 'vue/server-renderer'
import { describe, expect, it } from 'vitest'
import AppProfileAuth from './AppProfileAuth.vue'
const source = readFileSync(
new URL('./AppProfileAuth.vue', import.meta.url),
'utf8',
)
async function renderAuth(
mode: 'login' | 'register',
movingModeHighlight = true,
): Promise<string> {
return renderToString(
createSSRApp({
render: () =>
h(AppProfileAuth, {
avatarUrl: null,
body: 'Use the linked account.',
cameraLabel: 'Camera',
email: 'demo@ifruit.com',
emailLabel: 'SkyPic account',
error: '',
eyebrow: 'Your SkyPic account',
galleryLabel: 'Photos',
loginLabel: 'Continue to SkyPic',
loginModeLabel: 'Login',
mode,
movingModeHighlight,
pending: false,
registerLabel: 'Create profile',
registerModeLabel: 'Register',
title: 'Welcome back',
username: mode === 'login' ? 'alexm' : 'newprofile',
usernameLabel: 'Handle',
}),
}),
)
}
describe('AppProfileAuth', () => {
it('uses the rounded Sky UI moving highlight for its mode switch', async () => {
const html = await renderAuth('login')
expect(html).toContain('sky-segmented--strong')
expect(html).toContain('sky-segmented--rounded')
expect(html).toContain('sky-segmented__highlight')
expect(html).toContain('app-profile-auth__mode--moving-highlight')
expect(html).toContain('width:calc(50% - 4px)')
expect(html).toContain('--sky-segmented-indicator-offset:calc(0% + 0px)')
expect(html).toContain('aria-label="Your SkyPic account"')
})
it('moves the highlight and keeps short mode labels separate from actions', async () => {
const html = await renderAuth('register')
expect(html).toContain('--sky-segmented-indicator-offset:calc(100% + 4px)')
expect(html.match(/Login/g)).toHaveLength(1)
expect(html.match(/Register/g)).toHaveLength(1)
expect(html).toContain('Create profile')
expect(html).not.toContain('>Continue to SkyPic</button>')
})
it('keeps the moving highlight opt-in for SkyPic', async () => {
const html = await renderAuth('login', false)
expect(html).not.toContain('sky-segmented--strong')
expect(html).not.toContain('sky-segmented__highlight')
expect(html).toContain('app-profile-auth__mode-button--login')
expect(html).toContain('app-profile-auth__mode-button--active')
expect(source).toContain(':strong="movingModeHighlight"')
expect(source).toContain(
':not(.app-profile-auth__mode--moving-highlight)',
)
})
})
@@ -30,17 +30,14 @@ const props = withDefaults(
eyebrow: string eyebrow: string
galleryLabel: string galleryLabel: string
loginLabel: string loginLabel: string
loginModeLabel?: string
maxUsernameLength?: number maxUsernameLength?: number
minUsernameLength?: number minUsernameLength?: number
mode: 'login' | 'register' mode: 'login' | 'register'
movingModeHighlight?: boolean
password?: string password?: string
passwordLabel?: string passwordLabel?: string
passwordPlaceholder?: string passwordPlaceholder?: string
pending: boolean pending: boolean
registerLabel: string registerLabel: string
registerModeLabel?: string
requirePassword?: boolean requirePassword?: boolean
submitEnabled?: boolean submitEnabled?: boolean
title: string title: string
@@ -65,13 +62,10 @@ const props = withDefaults(
emailAsField: false, emailAsField: false,
maxUsernameLength: 40, maxUsernameLength: 40,
minUsernameLength: 2, minUsernameLength: 2,
loginModeLabel: '',
movingModeHighlight: false,
password: '', password: '',
passwordLabel: 'Password', passwordLabel: 'Password',
passwordPlaceholder: '', passwordPlaceholder: '',
requirePassword: false, requirePassword: false,
registerModeLabel: '',
showConfirmPassword: false, showConfirmPassword: false,
submitEnabled: undefined, submitEnabled: undefined,
usernameAutocomplete: 'username', usernameAutocomplete: 'username',
@@ -107,10 +101,6 @@ const canSubmit = computed(() => {
) )
) )
}) })
const modeLoginLabel = computed(() => props.loginModeLabel || props.loginLabel)
const modeRegisterLabel = computed(
() => props.registerModeLabel || props.registerLabel,
)
</script> </script>
<template> <template>
@@ -129,15 +119,9 @@ const modeRegisterLabel = computed(
<SkyGlass class="app-profile-auth__card"> <SkyGlass class="app-profile-auth__card">
<SkySegmented <SkySegmented
:active-index="movingModeHighlight ? (mode === 'login' ? 0 : 1) : undefined"
:aria-label="eyebrow"
:item-count="movingModeHighlight ? 2 : undefined"
raised raised
:rounded="movingModeHighlight"
:strong="movingModeHighlight"
class="app-profile-auth__mode" class="app-profile-auth__mode"
:class="{ :class="{
'app-profile-auth__mode--moving-highlight': movingModeHighlight,
'app-profile-auth__mode--register': mode === 'register', 'app-profile-auth__mode--register': mode === 'register',
}" }"
> >
@@ -149,7 +133,7 @@ const modeRegisterLabel = computed(
:active="mode === 'login'" :active="mode === 'login'"
@click="emit('update:mode', 'login')" @click="emit('update:mode', 'login')"
> >
{{ modeLoginLabel }} {{ loginLabel }}
</SkySegmentedButton> </SkySegmentedButton>
<SkySegmentedButton <SkySegmentedButton
class="app-profile-auth__mode-button app-profile-auth__mode-button--register" class="app-profile-auth__mode-button app-profile-auth__mode-button--register"
@@ -159,7 +143,7 @@ const modeRegisterLabel = computed(
:active="mode === 'register'" :active="mode === 'register'"
@click="emit('update:mode', 'register')" @click="emit('update:mode', 'register')"
> >
{{ modeRegisterLabel }} {{ registerLabel }}
</SkySegmentedButton> </SkySegmentedButton>
</SkySegmented> </SkySegmented>
@@ -388,24 +372,21 @@ const modeRegisterLabel = computed(
position: relative; position: relative;
z-index: 1; z-index: 1;
margin: 0 0 12px; margin: 0 0 12px;
}
.app-profile-auth__mode:not(.app-profile-auth__mode--moving-highlight) {
padding: 3px; padding: 3px;
border: 1px solid rgba(255, 255, 255, 0.08); border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 14px; border-radius: 14px;
background: rgba(0, 0, 0, 0.16); background: rgba(0, 0, 0, 0.16);
} }
.app-profile-auth__mode:not(.app-profile-auth__mode--moving-highlight) .app-profile-auth__mode :deep(.app-profile-auth__mode-button) {
:deep(.app-profile-auth__mode-button) {
border-radius: 3px; border-radius: 3px;
} }
.app-profile-auth__mode:not(.app-profile-auth__mode--moving-highlight) .app-profile-auth__mode
:deep( :deep(
.app-profile-auth__mode-button--login.app-profile-auth__mode-button--active .app-profile-auth__mode-button--login.app-profile-auth__mode-button--active
) { ) {
border-radius: 10px 3px 3px 10px; border-radius: 10px 3px 3px 10px;
} }
.app-profile-auth__mode:not(.app-profile-auth__mode--moving-highlight) .app-profile-auth__mode
:deep( :deep(
.app-profile-auth__mode-button--register.app-profile-auth__mode-button--active .app-profile-auth__mode-button--register.app-profile-auth__mode-button--active
) { ) {
@@ -704,8 +685,7 @@ const modeRegisterLabel = computed(
padding: 14px; padding: 14px;
border-radius: 28px; border-radius: 28px;
} }
.app-profile-auth--centered .app-profile-auth--centered .app-profile-auth__mode {
.app-profile-auth__mode:not(.app-profile-auth__mode--moving-highlight) {
display: grid; display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 4px; gap: 4px;
-9
View File
@@ -170,14 +170,6 @@ describe('app registry', () => {
expect(isPhoneAppId('music')).toBe(true) expect(isPhoneAppId('music')).toBe(true)
expect(isPhoneAppId('companies')).toBe(true) expect(isPhoneAppId('companies')).toBe(true)
expect(isPhoneAppId('weazel-news')).toBe(true) expect(isPhoneAppId('weazel-news')).toBe(true)
expect(PHONE_APPS.find((app) => app.id === 'skypic')).toMatchObject({
category: 'social',
dockOrder: null,
gridOrder: 31,
labelKey: 'Apps.skypic.name',
route: '/apps/skypic',
})
expect(isPhoneAppId('skypic')).toBe(true)
expect( expect(
PHONE_APPS.filter((app) => app.category === 'games').map((app) => app.id), PHONE_APPS.filter((app) => app.category === 'games').map((app) => app.id),
).toEqual([ ).toEqual([
@@ -196,7 +188,6 @@ describe('app registry', () => {
).toEqual([ ).toEqual([
'weazel-news', 'weazel-news',
'picstagram', 'picstagram',
'skypic',
'feather', 'feather',
'fliptok', 'fliptok',
'flare', 'flare',
-15
View File
@@ -75,7 +75,6 @@ import localPagesIcon from '@/assets/img/app-icons/local-pages.webp'
import flareIcon from '@/assets/img/app-icons/flare.webp' import flareIcon from '@/assets/img/app-icons/flare.webp'
import flipTokIcon from '@/assets/img/app-icons/fliptok.webp' import flipTokIcon from '@/assets/img/app-icons/fliptok.webp'
import picstagramIcon from '@/assets/img/app-icons/picstagram.webp' import picstagramIcon from '@/assets/img/app-icons/picstagram.webp'
import skyPicIcon from '@/assets/img/app-icons/skypic-v2.jpg'
import skyRideIcon from '@/assets/img/app-icons/skyride.webp' import skyRideIcon from '@/assets/img/app-icons/skyride.webp'
import musicIcon from '@/assets/img/app-icons/music.webp' import musicIcon from '@/assets/img/app-icons/music.webp'
import featherIcon from '@/assets/img/app-icons/feather.webp' import featherIcon from '@/assets/img/app-icons/feather.webp'
@@ -192,20 +191,6 @@ export const PHONE_APPS = shallowReactive<PhoneAppDefinition[]>([
labelKey: 'Apps.picstagram.name', labelKey: 'Apps.picstagram.name',
route: '/apps/picstagram', route: '/apps/picstagram',
}, },
{
category: 'social',
component: markRaw(
defineAsyncComponent(() => import('@/views/apps/SkyPicApp.vue')),
),
dockOrder: null,
gridOrder: 31,
icon: markRaw(Camera),
iconClass: 'app-icon--skypic',
iconImage: skyPicIcon,
id: 'skypic',
labelKey: 'Apps.skypic.name',
route: '/apps/skypic',
},
{ {
category: 'social', category: 'social',
component: markRaw( component: markRaw(
@@ -1,586 +0,0 @@
import { readFileSync } from 'node:fs'
import { describe, expect, it } from 'vitest'
const read = (path: string) =>
readFileSync(new URL(path, import.meta.url), 'utf8')
const server = read('../../sky_phone/source/server/skypic.lua')
const migration = read('../../sky_phone/source/server/db_migrate.lua')
const install = read('../../sky_phone/sql/install.sql')
const media = read('../../sky_phone/source/server/media.lua')
const config = read('../../sky_phone/config/config.lua')
const mediaUtils = read('./utils/media.ts')
const fallbackLocales = read('./stores/phone.ts')
const englishLocale = read('../../sky_phone/config/locales/en.lua')
const germanLocale = read('../../sky_phone/config/locales/de.lua')
const mockServer = read('../testserver/index.cjs')
function block(source: string, startMarker: string, endMarker: string): string {
const start = source.indexOf(startMarker)
const end = source.indexOf(endMarker, start + startMarker.length)
expect(start, `missing ${startMarker}`).toBeGreaterThanOrEqual(0)
expect(end, `missing ${endMarker}`).toBeGreaterThan(start)
return source.slice(start, end)
}
function migrationTable(name: string): string {
return block(migration, `name = "sky_phone_skypic_${name}"`, 'tableOptions =')
}
function installTable(name: string): string {
return block(
install,
`CREATE TABLE IF NOT EXISTS \`sky_phone_skypic_${name}\``,
') ENGINE=InnoDB',
)
}
const callbacks = [
'bootstrap',
'create-profile',
'delete-account',
'update-profile',
'search',
'add-friend',
'respond-friend',
'remove-friend',
'block',
'send-snap',
'open-snap',
'replay-snap',
'publish-story',
'stories',
'view-story',
'story-viewers',
'remove-story',
'spotlight-feed',
'publish-spotlight',
'view-spotlight',
'like-spotlight',
'spotlight-comments',
'comment-spotlight',
'delete-spotlight-comment',
'remove-spotlight',
'report-spotlight',
'thread',
'send-message',
'mark-thread',
'save-message',
'delete-message',
] as const
const tables: Record<string, string[]> = {
profiles: [
'id',
'account_id',
'handle',
'display_name',
'bio',
'avatar_media_id',
'avatar_seed',
'story_privacy',
'quick_add',
'allow_story_replies',
'snap_score',
'friend_count',
'status',
'created_at',
'updated_at',
],
friendships: [
'id',
'profile_a_id',
'profile_b_id',
'requested_by_id',
'status',
'profile_a_last_snap_on',
'profile_b_last_snap_on',
'streak_updated_on',
'streak_count',
'best_streak',
'accepted_at',
'created_at',
'updated_at',
],
blocks: ['blocker_profile_id', 'blocked_profile_id', 'created_at'],
messages: [
'id',
'friendship_id',
'sender_profile_id',
'recipient_profile_id',
'message_type',
'body',
'caption',
'overlay_text',
'overlay_color',
'media_id',
'view_seconds',
'allow_replay',
'read_at',
'opened_at',
'replayed_at',
'saved_at',
'expires_at',
'sender_deleted_at',
'recipient_deleted_at',
'deleted_at',
'created_at',
],
stories: [
'id',
'profile_id',
'media_id',
'caption',
'overlay_text',
'overlay_color',
'view_seconds',
'privacy',
'status',
'expires_at',
'created_at',
'updated_at',
],
story_views: ['story_id', 'viewer_profile_id', 'viewed_at'],
spotlights: [
'id',
'profile_id',
'media_id',
'caption',
'overlay_text',
'overlay_color',
'kind',
'ad_headline',
'comments_enabled',
'status',
'expires_at',
'created_at',
'updated_at',
],
spotlight_views: ['spotlight_id', 'viewer_profile_id', 'viewed_at'],
spotlight_likes: ['spotlight_id', 'profile_id', 'created_at'],
spotlight_comments: [
'id',
'spotlight_id',
'profile_id',
'body',
'status',
'created_at',
],
spotlight_reports: [
'spotlight_id',
'reporter_profile_id',
'reason',
'details',
'status',
'created_at',
],
}
describe('SkyPic backend contracts', () => {
it('keeps migration and clean-install schemas synchronized', () => {
for (const [table, columns] of Object.entries(tables)) {
const migrationSource = migrationTable(table)
const installSource = installTable(table)
for (const column of columns) {
expect(migrationSource, `${table}.${column} migration`).toContain(
`name = "${column}"`,
)
expect(installSource, `${table}.${column} install`).toContain(
`\`${column}\``,
)
}
}
for (const table of ['messages', 'stories', 'spotlights']) {
expect(migrationTable(table)).toContain('ON DELETE RESTRICT')
expect(installTable(table)).toContain('ON DELETE RESTRICT')
}
})
it('backfills every normalized unique key on existing databases', () => {
const afterMigrate = migration.slice(
migration.indexOf('Bridge.Database.Migrate("sky_phone", schema)'),
)
for (const index of [
'uniq_sky_phone_skypic_profile_account',
'uniq_sky_phone_skypic_profile_handle',
'uniq_sky_phone_skypic_friend_pair',
]) {
expect(afterMigrate).toContain(`"${index}"`)
}
expect(
afterMigrate.match(/\{ unique = true \}/g)?.length ?? 0,
).toBeGreaterThanOrEqual(3)
})
it('registers the complete canonical callback surface', () => {
for (const callback of callbacks) {
expect(server).toContain(
`Bridge.Callbacks.Register("sky_phone:skypic:${callback}"`,
)
}
expect(server).toContain('Bridge.Database.AfterMigration("sky_phone"')
})
it('validates all submitted media through the owned-media resolver', () => {
const editor = block(
server,
'local function editor_payload',
'Bridge.Callbacks.Register("sky_phone:skypic:thread"',
)
expect(editor).toContain(
'SkyPhoneMedia.ResolveOwnedMedia(source, media_id, data.mediaType)',
)
expect(
server.match(/SkyPhoneMedia\.ResolveOwnedMedia\(/g)?.length,
).toBeGreaterThanOrEqual(3)
expect(server).toContain(
'SkyPhoneMedia.ResolveOwnedMedia(source, avatar_media_id, "photo")',
)
})
it('validates dense photo batches before creating their cartesian snap set', () => {
const editors = block(
server,
'local function snap_editor_payloads',
'Bridge.Callbacks.Register("sky_phone:skypic:thread"',
)
expect(config).toContain('MaximumMediaPerSend = 10')
expect(config).toContain('MaximumSnapMessagesPerSend = 40')
expect(editors).toContain('count > limit("MaximumMediaPerSend", 10)')
expect(editors).toContain('if key_count ~= count then')
expect(editors).toContain('seen[media_id]')
expect(editors).toContain('mediaType = "photo"')
expect(server).toContain(
'SkyPhoneMedia.ResolveOwnedMedia(source, media_id, data.mediaType)',
)
const sendSnap = block(
server,
'Bridge.Callbacks.Register("sky_phone:skypic:send-snap"',
'Bridge.Callbacks.Register("sky_phone:skypic:open-snap"',
)
expect(sendSnap).toContain(
'raw_media_count > limit("MaximumMediaPerSend", 10)',
)
expect(sendSnap).toContain(
'message_count > limit("MaximumSnapMessagesPerSend", 40)',
)
expect(sendSnap).toContain('editor = editor')
expect(sendSnap).toContain('Bridge.Database.Transaction(statements)')
expect(sendSnap).toContain('SELECT COUNT(*) FROM')
expect(sendSnap).toContain(') <> ?')
expect(sendSnap).toContain(
'assertion_params[#assertion_params + 1] = #entries',
)
expect(sendSnap).toContain('message_ids[#message_ids + 1] = entry.id')
expect(sendSnap).toContain(
'local sent = load_snap_metadata(message_ids, profile.profile_id)',
)
expect(mockServer).toContain('function skyPicMediaItems(body)')
expect(mockServer).toContain('submitted.length > 10')
expect(mockServer).toContain('seen.has(mediaId)')
expect(mockServer).toContain('mediaItems.length * recipients.length > 40')
})
it('maintains reciprocal UTC-day streaks and returns reconciled values', () => {
const friendshipMigration = migrationTable('friendships')
const friendshipInstall = installTable('friendships')
const friends = block(
server,
'local function list_friends',
'local function list_requests',
)
const conversations = block(
server,
'local function list_conversations',
'Bridge.Callbacks.Register("sky_phone:skypic:bootstrap"',
)
const metadata = block(
server,
'local function load_snap_metadata',
'local function opened_snap_from_row',
)
const sendSnap = block(
server,
'Bridge.Callbacks.Register("sky_phone:skypic:send-snap"',
'Bridge.Callbacks.Register("sky_phone:skypic:open-snap"',
)
for (const schema of [friendshipMigration, friendshipInstall]) {
expect(schema).toContain('profile_a_last_snap_on')
expect(schema).toContain('profile_b_last_snap_on')
expect(schema).toContain('streak_updated_on')
expect(schema).toContain('streak_count')
expect(schema).toContain('best_streak')
expect(schema).toContain('idx_sky_phone_skypic_streaks')
}
expect(sendSnap).toContain('SET %s = UTC_DATE()')
expect(sendSnap).toContain(
'SELECT 1 FROM `sky_phone_skypic_messages` message WHERE message.`id` = ?',
)
expect(sendSnap).toContain('friendship.profile_a_id == profile.profile_id')
expect(sendSnap).toContain('`profile_a_last_snap_on` = UTC_DATE()')
expect(sendSnap).toContain('`profile_b_last_snap_on` = UTC_DATE()')
expect(sendSnap).toContain(
'`streak_updated_on` = DATE_SUB(UTC_DATE(), INTERVAL 1 DAY)',
)
expect(sendSnap).toContain(
'`streak_updated_on` IS NULL OR `streak_updated_on` < UTC_DATE()',
)
expect(sendSnap).toContain('Bridge.Database.Transaction(statements)')
for (const serializer of [friends, conversations, metadata]) {
expect(serializer).toContain(
'`streak_updated_on` < DATE_SUB(UTC_DATE(), INTERVAL 1 DAY)',
)
expect(serializer).toContain(
'THEN 0 ELSE friendship.`streak_count` END AS `streak_count`',
)
expect(serializer).toContain('friendship.`best_streak`')
}
expect(metadata).toContain("friendship.`status` = 'accepted'")
expect(metadata).toContain(
'snap.streakCount = tonumber(row.streak_count) or 0',
)
expect(metadata).toContain(
'snap.bestStreak = tonumber(row.best_streak) or 0',
)
expect(server).toContain('SET `streak_count` = 0')
expect(server).toContain(
"WHERE `status` = 'accepted' AND `streak_count` > 0",
)
})
it('keeps browser payload limits and profile creation aligned with production', () => {
expect(config).toContain('CaptionMaxLength = 160')
expect(server).toContain('valid_integer(data.avatarSeed, 1, 2147483647)')
expect(mockServer).toContain('candidate <= 2_147_483_647')
expect(mockServer).toContain(
'return [...caption].length <= 160 ? caption : null',
)
expect(mockServer).toContain("error: 'invalid_avatar_seed'")
expect(mockServer).toContain("error: 'invalid_caption'")
expect(mockServer).toContain('value.length > 20')
expect(mockServer).toContain("error: 'message_too_long'")
expect(mockServer).not.toContain('body.slice(0, 1000)')
expect(mockServer).toContain("error: 'profile_exists'")
expect(mockServer).toContain(
"const onboardingScenario = testScenario === 'skypic-onboarding'",
)
expect(mockServer).toContain(
'onboardingScenario && skyPicOnboardingProfile',
)
expect(mockServer).toContain('const profile = skyPicOnboardingProfile')
expect(mockServer).toContain('suggestions: profile')
})
it('deletes only the confirmed SkyPic account and silently refreshes peers', () => {
const deletion = block(
server,
'Bridge.Callbacks.Register("sky_phone:skypic:delete-account"',
'Bridge.Callbacks.Register("sky_phone:skypic:update-profile"',
)
expect(deletion).toContain('data.confirmed ~= true')
expect(deletion).toContain('error = "confirmation_required"')
expect(deletion).toContain('Bridge.Database.Transaction({')
expect(deletion).toContain('SET peer.')
expect(deletion).toContain('friend_count')
expect(deletion).toContain(' > 0, peer.')
expect(deletion).toContain('- 1, 0')
expect(deletion).toContain('DELETE FROM')
expect(deletion).toContain('sky_phone_skypic_profiles')
expect(deletion).toContain('sky_phone_skypic_blocks')
expect(deletion).toContain('UNION ALL')
expect(deletion).toContain(
"AND status = 'active'".replace(
'status',
String.fromCharCode(96) + 'status' + String.fromCharCode(96),
),
)
expect(deletion).not.toContain('sky_phone_media')
expect(deletion).toContain('"sky_phone:skypic:changed"')
expect(deletion).not.toContain('"sky_phone:skypic:new"')
expect(mockServer).toContain("if (endpoint === 'skypic:delete-account')")
expect(mockServer).toContain("error: 'confirmation_required'")
})
it('keeps direct snap secrets out of bootstrap and thread serializers', () => {
const safeSnap = block(
server,
'local function safe_snap_from_row',
'local function text_message_from_row',
)
for (const secret of [
'url = row.url',
'caption =',
'textOverlay =',
'overlayColor =',
]) {
expect(safeSnap).not.toContain(secret)
}
const thread = block(
server,
'local function list_thread',
'local function editor_payload',
)
expect(thread).not.toContain('message.`caption`')
expect(thread).not.toContain('message.`overlay_text`')
expect(thread).not.toContain('message.`overlay_color`')
expect(thread).not.toContain('message.`media_id`')
const storyList = block(
server,
'local function list_stories',
'local function list_conversations',
)
expect(storyList).not.toContain('story.`caption`')
expect(storyList).not.toContain('story.`overlay_text`')
expect(storyList).not.toContain('story.`overlay_color`')
expect(storyList).not.toContain('story.`media_id`')
expect(storyList).toContain(
'ORDER BY (story.`profile_id` = ?) DESC, story.`created_at` DESC, story.`id` DESC',
)
expect(storyList).not.toContain(
'ORDER BY (story.`profile_id` = ?) DESC, `seen`',
)
})
it('releases snap contents only after atomic one-time state changes', () => {
const open = block(
server,
'Bridge.Callbacks.Register("sky_phone:skypic:open-snap"',
'Bridge.Callbacks.Register("sky_phone:skypic:replay-snap"',
)
expect(open).toContain('message.`opened_at` IS NULL')
expect(open).toContain('message.`recipient_profile_id` = ?')
expect(open).toContain(
'IF(message.`allow_replay` = 1, ?, message.`view_seconds`)',
)
expect(open.indexOf('affected_rows(result) ~= 1')).toBeLessThan(
open.indexOf('released_snap('),
)
const replay = block(
server,
'Bridge.Callbacks.Register("sky_phone:skypic:replay-snap"',
'local function own_story_metadata',
)
expect(replay).toContain('message.`allow_replay` = 1')
expect(replay).toContain('message.`opened_at` IS NOT NULL')
expect(replay).toContain('message.`replayed_at` IS NULL')
expect(replay.indexOf('affected_rows(result) ~= 1')).toBeLessThan(
replay.indexOf('released_snap('),
)
})
it('atomically protects every database media reference before remote delete', () => {
const guard = block(
media,
'local function is_referenced_by_skypic',
'local function delete_owned_media',
)
expect(guard).toContain('FROM `sky_phone_skypic_messages`')
expect(guard).toContain('FROM `sky_phone_skypic_stories`')
expect(guard).toContain('FROM `sky_phone_skypic_spotlights`')
expect(guard).not.toContain('`expires_at` >')
expect(guard).not.toContain("`status` = 'active'")
const deletion = block(
media,
'local function delete_owned_media',
'RegisterNetEvent("sky_phone:media:delete"',
)
expect(deletion).toContain('return false, "media_in_use"')
expect(deletion).toContain('AND NOT EXISTS (')
expect(deletion).toContain('if affected_rows(result) ~= 1 then')
expect(deletion.indexOf('DELETE FROM `sky_phone_media`')).toBeLessThan(
deletion.indexOf('delete_remote_file(row.remote_id)'),
)
expect(mediaUtils).toContain("'media_in_use'")
expect(fallbackLocales).toMatch(
/media_in_use:\s*'This media is still used by SkyPic and cannot be deleted yet\.'/,
)
expect(englishLocale).toContain(
'media_in_use = "This media is still used by SkyPic and cannot be deleted yet."',
)
expect(germanLocale).toContain(
'media_in_use = "Dieses Medium wird noch von SkyPic verwendet und kann noch nicht gelöscht werden."',
)
})
it('enforces viewer-specific deletion and bounded expiry cleanup', () => {
expect(server).toContain('`sender_deleted_at`')
expect(server).toContain('`recipient_deleted_at`')
expect(server).toContain('SET `deleted_at` = CURRENT_TIMESTAMP(6)')
expect(server).toContain('AND `sender_profile_id` = ?')
expect(server).toContain('Wait(limit("CleanupIntervalSeconds", 45) * 1000)')
expect(config).toContain('StoryLifetimeSeconds = 24 * 60 * 60')
expect(config).toContain('ReplayWindowSeconds = 5 * 60')
expect(config).toContain('TextAfterReadLifetimeSeconds = 24 * 60 * 60')
})
it('gates optional story replies in the message insert itself', () => {
const sendMessage = block(
server,
'Bridge.Callbacks.Register("sky_phone:skypic:send-message"',
'Bridge.Callbacks.Register("sky_phone:skypic:mark-thread"',
)
expect(sendMessage).toContain('local story_id =')
expect(sendMessage).toContain('story.`profile_id` = ?')
expect(sendMessage).toContain("story.`status` = 'active'")
expect(sendMessage).toContain('story.`expires_at` > CURRENT_TIMESTAMP(6)')
expect(sendMessage).toContain('author.`allow_story_replies` = 1')
expect(sendMessage).toContain("friendship.`status` = 'accepted'")
expect(sendMessage).toContain('NOT EXISTS (')
expect(sendMessage).toContain('story_id and "story_reply" or "message"')
})
it('returns a rich outgoing friend request from add-friend', () => {
const addFriend = block(
server,
'Bridge.Callbacks.Register("sky_phone:skypic:add-friend"',
'Bridge.Callbacks.Register("sky_phone:skypic:respond-friend"',
)
expect(addFriend).toContain('target.friendshipId = friendship_id')
expect(addFriend).toContain('target.friendshipStatus = "outgoing"')
expect(addFriend).toContain('direction = "outgoing"')
expect(addFriend).toContain('profile = target')
})
it('enforces the friend limit atomically while accepting requests', () => {
const respondFriend = block(
server,
'Bridge.Callbacks.Register("sky_phone:skypic:respond-friend"',
'Bridge.Callbacks.Register("sky_phone:skypic:remove-friend"',
)
expect(respondFriend).toContain(
'profile_a.`friend_count` = profile_a.`friend_count` + 1',
)
expect(respondFriend).toContain(
'profile_b.`friend_count` = profile_b.`friend_count` + 1',
)
expect(respondFriend).toContain('profile_a.`friend_count` < ?')
expect(respondFriend).toContain('profile_b.`friend_count` < ?')
expect(respondFriend).toContain(
'return { success = false, error = "friend_limit_reached" }',
)
})
it('keeps pending requests out of quick-add suggestions', () => {
const profiles = block(
server,
'local function list_profiles',
'local function list_conversations',
)
expect(profiles).toContain(
'filters[#filters + 1] = "friendship.`id` IS NULL"',
)
expect(profiles).not.toContain("friendship.`status` = 'pending'")
})
})
@@ -1,196 +0,0 @@
import { readFileSync } from 'node:fs'
import { describe, expect, it } from 'vitest'
const read = (path: string) =>
readFileSync(new URL(path, import.meta.url), 'utf8')
const app = read('./App.vue')
const appAuth = read('./stores/app-auth.ts')
const appIcon = read('./components/AppIcon.vue')
const client = read('../../sky_phone/source/client/main.lua')
const easyShare = read('../../sky_phone/source/server/easyshare.lua')
const manifest = read('../../sky_phone/fxmanifest.lua')
const mockServer = read('../testserver/index.cjs')
const phoneServer = read('../../sky_phone/source/server/phone.lua')
const reservedApps = read('../../sky_phone/source/shared/custom_apps.lua')
const server = read('../../sky_phone/source/server/skypic.lua')
const types = read('./types/skypic.ts')
const callbacks = [
'skypic:bootstrap',
'skypic:create-profile',
'skypic:delete-account',
'skypic:update-profile',
'skypic:search',
'skypic:add-friend',
'skypic:respond-friend',
'skypic:remove-friend',
'skypic:block',
'skypic:send-snap',
'skypic:open-snap',
'skypic:replay-snap',
'skypic:publish-story',
'skypic:stories',
'skypic:view-story',
'skypic:story-viewers',
'skypic:remove-story',
'skypic:spotlight-feed',
'skypic:publish-spotlight',
'skypic:view-spotlight',
'skypic:like-spotlight',
'skypic:spotlight-comments',
'skypic:comment-spotlight',
'skypic:delete-spotlight-comment',
'skypic:remove-spotlight',
'skypic:report-spotlight',
'skypic:thread',
'skypic:send-message',
'skypic:mark-thread',
'skypic:save-message',
'skypic:delete-message',
] as const
function typeBlock(name: string): string {
const start = types.indexOf(`export type ${name} = {`)
const end = types.indexOf(String.fromCharCode(10) + '}', start)
expect(start).toBeGreaterThanOrEqual(0)
expect(end).toBeGreaterThan(start)
return types.slice(start, end)
}
describe('SkyPic cross-runtime integration contract', () => {
it('bridges every canonical callback through client, server, and browser mock', () => {
for (const callback of callbacks) {
expect(client, `missing client callback ${callback}`).toContain(
`"${callback}"`,
)
expect(server, `missing server callback ${callback}`).toContain(
`"sky_phone:${callback}"`,
)
expect(mockServer, `missing browser mock ${callback}`).toContain(
`endpoint === '${callback}'`,
)
}
})
it('loads the server after media and reserves the app for built-in sharing', () => {
const mediaIndex = manifest.indexOf("'source/server/media.lua'")
const skyPicIndex = manifest.indexOf("'source/server/skypic.lua'")
expect(mediaIndex).toBeGreaterThanOrEqual(0)
expect(skyPicIndex).toBeGreaterThan(mediaIndex)
expect(reservedApps).toContain('skypic = true')
expect(easyShare).toContain('skypic = true')
})
it('routes localized device-aware notifications and refreshes live state', () => {
expect(client).toContain(
'RegisterNetEvent("sky_phone:skypic:new", function(data)',
)
expect(client).toContain('locale.Nui.Apps.skypic')
expect(client).toContain(
'notification_text:gsub("{actor}", tostring(data.actor or ""))',
)
expect(client).toContain(
'SendNUIMessage({ type = "skypic:new", data = data })',
)
expect(client).toContain(
'RegisterNetEvent("sky_phone:skypic:changed", function(data)',
)
expect(client).toContain(
'SendNUIMessage({ type = "skypic:changed", data = data })',
)
expect(server).toContain(
'notify_profile(friendship.peer_id, profile, story_id and "story_reply" or "message", nil)',
)
expect(server).toContain('profileId = actor.profile_id')
expect(phoneServer).toContain('required_app_auth')
expect(phoneServer).toContain(
'app_auth.accountEmail ~= device.account_email',
)
expect(phoneServer).toContain('has_required_app_session(device)')
expect(server.match(/}, 'skypic'\)/g)).toHaveLength(3)
expect(app).toContain("event.data?.type === 'skypic:new'")
expect(app).toContain("event.data?.type === 'skypic:changed'")
expect(app).toContain("appId: 'skypic'")
expect(app).toContain('route: skyPicNotificationRoute(data)')
expect(app).toContain("query.set('profileId', data.profileId)")
expect(app).toContain(
"if (data.kind === 'snap' && data.snapId) query.set('snap', data.snapId)",
)
expect(app).toContain(
'preferences: parsePhonePreferences(data.device.settings ?? null)',
)
expect(app).toContain(
'!data.device || data.device.imei === phone.device?.imei',
)
expect(app).toContain('if (phone.isOpen && targetsActiveDevice)')
expect(app).toContain('void refreshSkyPicState(true)')
expect(app).toContain('!targetsActiveDevice || signedInOnActiveDevice')
expect(appIcon).toContain(
"if (props.app.id === 'skypic') return skypic.unreadCount",
)
})
it('scopes badge state to the active unlocked device and account session', () => {
expect(appAuth).toContain("'skypic'")
expect(app).toContain(
"if (!account.email || !appAuth.isSignedIn('skypic'))",
)
expect(app).toContain(
"() => [phone.device?.imei ?? '', account.email] as const",
)
expect(app).toContain(
'if (imei === previousImei && email === previousEmail) return',
)
expect(app).toContain('skypic.resetSession()')
expect(app).toContain("appAuth.signOut('skypic')")
expect(app).toContain('unlockedServicesLoaded.value = false')
expect(app).toContain(
'if (phone.isOpen && !isLocked.value && !setupRequired.value)',
)
})
it('does not let background refreshes abort auth discovery or account deletion', () => {
const refreshBlock = app
.split('async function refreshSkyPicState(refreshThread = false)')[1]
?.split('function queueCompaniesChange')[0]
expect(refreshBlock).toContain(
'if (!skypic.bootstrapPending) skypic.resetSession()',
)
expect(refreshBlock).toContain('if (skypic.accountDeletePending) return')
expect(app).toContain('!skypic.bootstrapPending')
})
it('keeps snap and story secrets out of list payload types', () => {
const snap = typeBlock('SkyPicSnap')
const story = typeBlock('SkyPicStory')
const openedSnap = typeBlock('SkyPicOpenedSnap')
const viewedStory = typeBlock('SkyPicViewedStory')
for (const metadata of [snap, story]) {
expect(metadata).not.toContain('url:')
expect(metadata).not.toContain('caption:')
expect(metadata).not.toContain('textOverlay:')
expect(metadata).not.toContain('overlayColor:')
}
for (const opened of [openedSnap, viewedStory]) {
expect(opened).toContain('url:')
expect(opened).toContain('caption:')
expect(opened).toContain('textOverlay:')
expect(opened).toContain('overlayColor:')
}
expect(viewedStory).toContain('canReply:')
expect(mockServer).toContain('const skyPicSnapContents = new Map(')
expect(mockServer).toContain('const skyPicStoryContents = new Map(')
expect(mockServer).toContain('blockedProfiles: skyPicProfiles')
expect(mockServer).toContain(
'skyPicIncrementOwnScore(recipients.length * mediaItems.length)',
)
expect(mockServer).toContain('.slice(offset, offset + 30)')
expect(mockServer).toContain(
"response.json({ success: false, error: 'story_unavailable' })",
)
})
})
-18
View File
@@ -37,24 +37,6 @@ describe('app auth store', () => {
}) })
}) })
it('persists the SkyPic session independently', () => {
const auth = useAppAuthStore()
auth.hydrate(null, 'demo@ifruit.com')
auth.signIn('skypic', 'demo@ifruit.com')
expect(auth.isSignedIn('skypic')).toBe(true)
expect(auth.isSignedIn('feather')).toBe(false)
expect(saveDeviceNamespace).toHaveBeenLastCalledWith('appAuth', {
accountEmail: 'demo@ifruit.com',
signedIn: ['skypic'],
version: 1,
})
auth.signOut('skypic')
expect(auth.isSignedIn('skypic')).toBe(false)
})
it('does not restore sessions belonging to another iFruit account', () => { it('does not restore sessions belonging to another iFruit account', () => {
const auth = useAppAuthStore() const auth = useAppAuthStore()
auth.hydrate( auth.hydrate(
-2
View File
@@ -7,7 +7,6 @@ export const APP_AUTH_IDS = [
'local-pages', 'local-pages',
'feather', 'feather',
'crewlink', 'crewlink',
'skypic',
] as const ] as const
export type AppAuthId = (typeof APP_AUTH_IDS)[number] export type AppAuthId = (typeof APP_AUTH_IDS)[number]
@@ -24,7 +23,6 @@ function emptySessions(): Record<AppAuthId, boolean> {
'local-pages': false, 'local-pages': false,
feather: false, feather: false,
crewlink: false, crewlink: false,
skypic: false,
} }
} }
-236
View File
@@ -209,242 +209,6 @@ describe('phone locale fallback', () => {
) )
}) })
it('keeps the complete SkyPic copy translated with a partial server locale', () => {
const phone = usePhoneStore()
phone.open({ locales: { Apps: { skypic: { name: 'SkyPic' } } } })
const skyPicKeys = [
'name',
'loading',
'navigation',
...['seconds', 'minutes', 'hours', 'days'].map((key) => 'time.' + key),
...[
'title',
'eyebrow',
'heading',
'body',
'displayName',
'displayNamePlaceholder',
'handle',
'handlePlaceholder',
'accountBound',
'create',
'creating',
].map((key) => 'onboarding.' + key),
...['title', 'body', 'eyebrow', 'login', 'loggingIn', 'noAccount'].map(
(key) => 'auth.' + key,
),
...[
'eyebrow',
'title',
'body',
'snap',
'story',
'photo',
'video',
'gallery',
'capturePhoto',
'captureVideo',
'snapHint',
'storyHint',
].map((key) => 'camera.' + key),
...[
'snapTitle',
'storyTitle',
'send',
'addStory',
'caption',
'captionPlaceholder',
'textOverlay',
'textPlaceholder',
'color',
'duration',
'seconds',
'replay',
'replayBody',
'recipients',
'recipientsHint',
'recipientLimit',
'selectedCount',
'noFriends',
'changeMedia',
'sent',
'storyPublished',
].map((key) => 'composer.' + key),
...['camera', 'chats', 'stories', 'friends'].map((key) => 'tabs.' + key),
...[
'title',
'incoming',
'noSnaps',
'conversations',
'noConversations',
'start',
'message',
'threadPlaceholder',
'messageLimit',
'saved',
'save',
'unsave',
'delete',
'sendSnap',
'moreActions',
'attachPhoto',
'takePhoto',
'emoji',
'attachVideo',
'attachmentPreview',
'removeAttachment',
'moveAttachmentEarlier',
'moveAttachmentLater',
'attachmentLimit',
'sendingAttachments',
'photoAttachmentsSent',
'sending',
'failed',
].map((key) => 'chats.' + key),
...[
'newVideo',
'newPhoto',
'replayed',
'opened',
'video',
'photo',
'replay',
].map((key) => 'snaps.' + key),
...[
'title',
'add',
'yours',
'friends',
'emptyTitle',
'emptyBody',
'views',
'viewers',
'noViewers',
'replyPlaceholder',
'replySent',
'replyLimit',
'delete',
'deleted',
'seen',
'unseen',
].map((key) => 'stories.' + key),
...[
'title',
'searchPlaceholder',
'searchResults',
'score',
'requests',
'sentRequests',
'accept',
'decline',
'quickAdd',
'add',
'pending',
'cancelRequest',
'all',
'empty',
'remove',
'block',
'blockedProfiles',
'unblock',
'chat',
'sendSnap',
'respond',
'friends',
'requestSent',
'requestCanceled',
'removed',
'blocked',
'unblocked',
].map((key) => 'friends.' + key),
...[
'title',
'edit',
'save',
'cancel',
'score',
'streaks',
'friends',
'bio',
'bioPlaceholder',
'storyPrivacy',
'privacyFriends',
'privacyEveryone',
'allowStoryReplies',
'allowStoryRepliesBody',
'showInQuickAdd',
'showInQuickAddBody',
'saved',
'account',
'logout',
'logoutTitle',
'logoutBody',
'loggingOut',
'deleteAccount',
'deleteAccountTitle',
'deleteAccountBody',
'deletingAccount',
].map((key) => 'profile.' + key),
...['close', 'timeLeft'].map((key) => 'viewer.' + key),
...[
'friend_request',
'friend_accepted',
'snap',
'message',
'story_reply',
'snap_opened',
'default',
].map((key) => 'notifications.' + key),
...[
'profile_required',
'profile_exists',
'invalid_handle',
'handle_taken',
'invalid_display_name',
'invalid_bio',
'invalid_avatar',
'invalid_avatar_seed',
'invalid_privacy',
'invalid_request',
'profile_not_found',
'blocked',
'friendship_not_found',
'friend_request_exists',
'friend_limit_reached',
'request_limit_reached',
'invalid_recipients',
'invalid_media',
'invalid_media_type',
'invalid_duration',
'invalid_caption',
'invalid_overlay',
'invalid_color',
'snap_unavailable',
'replay_unavailable',
'message_empty',
'message_too_long',
'message_not_found',
'story_limit_reached',
'story_unavailable',
'not_authorized',
'confirmation_required',
'too_many_snaps',
'rate_limited',
'request_timeout',
'request_failed',
'not_authenticated',
'unknown_error',
'default',
].map((key) => 'errors.' + key),
]
for (const key of skyPicKeys) {
const path = 'Apps.skypic.' + key
expect(phone.t(path), path).not.toBe(path)
}
})
it('uses the English Lua payload before the bundled emergency fallback', () => { it('uses the English Lua payload before the bundled emergency fallback', () => {
const phone = usePhoneStore() const phone = usePhoneStore()
phone.open({ phone.open({
+36 -490
View File
@@ -1643,306 +1643,6 @@ const defaultLocales: LocaleTree = {
default: 'Picstagram could not complete the request.', default: 'Picstagram could not complete the request.',
}, },
}, },
skypic: {
name: 'SkyPic',
loading: 'Loading SkyPic',
navigation: 'SkyPic navigation',
time: {
seconds: '{count}s ago',
minutes: '{count}m ago',
hours: '{count}h ago',
days: '{count}d ago',
},
onboarding: {
title: 'Welcome to SkyPic',
eyebrow: 'Your private camera network',
heading: 'Share the moment',
body: 'Create a profile for private snaps, close-friend stories, Spotlight videos, and quick chats.',
displayName: 'Display name',
displayNamePlaceholder: 'Your name',
handle: 'Handle',
handlePlaceholder: 'your.handle',
accountBound:
'Your SkyPic profile stays linked to this Sky Cloud account.',
create: 'Create profile',
creating: 'Creating...',
},
auth: {
eyebrow: 'Your SkyPic account',
title: 'Welcome back',
body: 'Continue with your iFruit account to access your SkyPic profile, snaps, and chats.',
login: 'Continue to SkyPic',
loggingIn: 'Signing in...',
noAccount: 'New to SkyPic? Create an iFruit account to get started.',
},
camera: {
eyebrow: 'Sky camera',
title: 'Capture',
body: 'Take a photo or video and share it for just a few seconds.',
snap: 'Snap',
story: 'Story',
photo: 'Photo',
video: 'Video',
gallery: 'Open gallery',
capturePhoto: 'Take photo',
captureVideo: 'Record video',
snapHint: 'Send it privately to one or more friends.',
storyHint: 'Share it with your story for 24 hours.',
},
composer: {
snapTitle: 'New snap',
storyTitle: 'New story',
spotlightTitle: 'New Spotlight',
send: 'Send',
addStory: 'Add to story',
publishSpotlight: 'Publish',
caption: 'Caption',
captionPlaceholder: 'Add a caption...',
textOverlay: 'Text overlay',
textPlaceholder: 'Put words on the moment...',
color: 'Text color',
duration: 'View duration',
seconds: '{count} seconds',
replay: 'Allow one replay',
replayBody: 'Recipients may open this snap one additional time.',
recipients: 'Recipients',
recipientsHint: 'Choose one or more friends.',
recipientLimit: 'Choose up to {count} friends.',
selectedCount: '{count} selected',
noFriends: 'Add a friend before sending a snap.',
sponsored: 'Sponsored post',
sponsoredBody: 'Clearly label this Spotlight as advertising.',
adHeadline: 'Ad headline',
adHeadlinePlaceholder: 'What should people discover?',
allowComments: 'Allow comments',
allowCommentsBody: 'People can comment on this Spotlight.',
changeMedia: 'Change photo or video',
sent: 'Snap sent.',
storyPublished: 'Story published.',
spotlightPublished: 'Spotlight published.',
},
tabs: {
camera: 'Camera',
chats: 'Chats',
stories: 'Stories',
friends: 'Friends',
spotlight: 'Spotlight',
},
spotlight: {
title: 'Spotlight',
add: 'Create Spotlight',
empty: 'No Spotlight videos yet',
emptyBody: 'Record the first short video for the SkyPic community.',
videoBy: 'Spotlight video by {name}',
sponsored: 'Sponsored',
viewProfile: 'View creator profile',
like: 'Like',
comments: 'Comments',
views: 'Views',
noComments: 'No comments yet',
commentPlaceholder: 'Write a comment...',
sendComment: 'Send',
commentsDisabled: 'Comments are disabled for this Spotlight.',
deleteComment: 'Delete comment',
delete: 'Delete Spotlight',
deleted: 'Spotlight deleted.',
report: 'Report Spotlight',
reportBody: 'Tell us why this Spotlight should be reviewed.',
submitReport: 'Submit report',
reported: 'Spotlight reported.',
navigation: 'Spotlight navigation',
previous: 'Previous Spotlight',
next: 'Next Spotlight',
reportReasons: {
spam: 'Spam or misleading',
harassment: 'Harassment',
dangerous: 'Dangerous content',
illegal: 'Illegal content',
other: 'Other',
},
},
chats: {
title: 'Chats',
incoming: 'New snaps',
noSnaps: 'No unopened snaps',
conversations: 'Conversations',
noConversations: 'Your conversations will appear here.',
start: 'Start a conversation',
message: 'Message',
threadPlaceholder: 'Write a message...',
messageLimit: 'Messages can contain up to {count} characters.',
saved: 'Saved in chat',
save: 'Save',
unsave: 'Unsave',
delete: 'Delete',
sendSnap: 'Send a snap',
moreActions: 'More actions',
attachPhoto: 'Attach photos',
takePhoto: 'Take photo',
emoji: 'Emoji',
attachVideo: 'Attach video',
attachmentPreview: 'Selected attachments',
removeAttachment: 'Remove attachment {number}',
moveAttachmentEarlier: 'Move attachment {number} earlier',
moveAttachmentLater: 'Move attachment {number} later',
attachmentLimit: 'You can attach up to {count} photos.',
sendingAttachments: 'Sending photos...',
photoAttachmentsSent: 'Photos sent.',
sending: 'Sending...',
failed: 'Not delivered',
},
snaps: {
newVideo: 'New video snap',
newPhoto: 'New photo snap',
replayed: 'Replayed',
opened: 'Opened',
video: 'Video snap',
photo: 'Photo snap',
replay: 'Replay snap',
},
stories: {
title: 'Stories',
add: 'Add story',
yours: 'Your story',
friends: 'Friends',
emptyTitle: 'No stories right now',
emptyBody: 'Stories from your friends will appear here for 24 hours.',
views: '{count} views',
viewers: 'Viewers',
noViewers: 'No views yet',
replyPlaceholder: 'Reply to this story...',
replySent: 'Reply sent.',
replyLimit: 'Replies can contain up to {count} characters.',
delete: 'Delete story',
deleted: 'Story deleted.',
seen: 'Seen',
unseen: 'New',
},
friends: {
title: 'Friends',
searchPlaceholder: 'Search name or @handle',
searchResults: 'Search results',
score: '{count} points',
requests: 'Friend requests',
sentRequests: 'Sent requests',
accept: 'Accept',
decline: 'Decline',
quickAdd: 'Quick Add',
add: 'Add',
pending: 'Pending',
cancelRequest: 'Cancel request',
all: 'Your friends',
empty: 'Add friends to start snapping.',
remove: 'Remove friend',
block: 'Block',
blockedProfiles: 'Blocked profiles',
unblock: 'Unblock',
chat: 'Chat',
sendSnap: 'Send snap',
respond: 'Respond',
friends: 'Friends',
requestSent: 'Friend request sent to {name}.',
requestCanceled: 'Friend request to {name} canceled.',
removed: '{name} was removed from your friends.',
blocked: '{name} was blocked.',
unblocked: '{name} was unblocked.',
},
profile: {
title: 'Profile',
edit: 'Edit profile',
save: 'Save',
cancel: 'Cancel',
score: 'Snap score',
streaks: 'Streaks',
friends: 'Friends',
bio: 'Bio',
bioPlaceholder: 'Tell your friends a little about you...',
storyPrivacy: 'Story privacy',
privacyFriends: 'Friends only',
privacyEveryone: 'Everyone',
allowStoryReplies: 'Allow story replies',
allowStoryRepliesBody: 'Friends can reply to your stories in chat.',
showInQuickAdd: 'Show in Quick Add',
showInQuickAddBody: 'Let other profiles discover you as a suggestion.',
saved: 'Profile updated.',
account: 'Account',
logout: 'Sign out',
logoutTitle: 'Sign out of SkyPic?',
logoutBody:
'You will only be signed out of SkyPic. Your profile, snaps, and chats stay available.',
loggingOut: 'Signing out...',
deleteAccount: 'Delete SkyPic account',
deleteAccountTitle: 'Delete your SkyPic account?',
deleteAccountBody:
'Your SkyPic profile, friends, snaps, stories, Spotlights, comments, and chats will be permanently deleted. Your iFruit account and Photos library stay available.',
deletingAccount: 'Deleting account...',
},
viewer: {
close: 'Close',
timeLeft: '{count}s',
},
notifications: {
friend_request: '{actor} sent you a friend request.',
friend_accepted: '{actor} accepted your friend request.',
snap: '{actor} sent you a snap.',
message: '{actor} sent you a message.',
story_reply: '{actor} replied to your story.',
snap_opened: '{actor} opened your snap.',
default: 'You have new SkyPic activity.',
},
errors: {
profile_required: 'Create your SkyPic profile first.',
profile_exists: 'This account already has a SkyPic profile.',
invalid_handle: 'Use 3-24 letters, numbers, dots, or underscores.',
handle_taken: 'This handle is already taken.',
invalid_display_name: 'Enter a display name.',
invalid_bio: 'Your bio is too long.',
invalid_avatar: 'Choose a valid profile photo.',
invalid_avatar_seed: 'Choose a valid avatar.',
invalid_privacy: 'Choose a valid story privacy setting.',
invalid_request: 'This friend request is invalid.',
profile_not_found: 'This SkyPic profile is unavailable.',
blocked: 'This profile is blocked.',
friendship_not_found: 'This friendship is unavailable.',
friend_request_exists: 'A friend request already exists.',
friend_limit_reached: 'Your friends list is full.',
request_limit_reached: 'Too many open friend requests.',
invalid_recipients: 'Choose at least one current friend.',
invalid_media: 'Choose media from this phone.',
invalid_media_type: 'This media type is not supported.',
invalid_duration: 'Choose a view time from 1 to 10 seconds.',
invalid_caption: 'This caption is too long.',
invalid_overlay: 'This text overlay is too long.',
invalid_color: 'Choose a valid overlay color.',
snap_unavailable: 'This snap is no longer available.',
replay_unavailable: 'This snap cannot be replayed again.',
message_empty: 'Write a message before sending.',
message_too_long: 'Messages can contain at most 2000 characters.',
message_not_found: 'This message is unavailable.',
story_limit_reached: 'Your active story limit is reached.',
story_unavailable: 'This story is no longer available.',
spotlight_unavailable: 'This Spotlight is no longer available.',
spotlight_limit_reached: 'Your active Spotlight limit is reached.',
sponsored_limit_reached:
'Your active sponsored Spotlight limit is reached.',
ads_disabled: 'Sponsored Spotlights are disabled.',
invalid_ad_headline: 'Enter an ad headline with 3 to 80 characters.',
invalid_comment: 'Enter a valid comment.',
comments_disabled: 'Comments are disabled.',
comment_not_found: 'This comment is unavailable.',
invalid_report: 'Choose a valid report reason.',
not_authorized: 'You are not allowed to do that.',
confirmation_required:
'Confirm that you want to delete your SkyPic account.',
too_many_snaps: 'Choose fewer photos or recipients.',
rate_limited: 'Slow down for a moment and try again.',
request_timeout: 'The SkyPic request timed out. Try again.',
request_failed: 'SkyPic could not complete the request.',
not_authenticated: 'Sign in to Sky Cloud first.',
unknown_error: 'SkyPic could not complete the request.',
default: 'SkyPic could not complete the request.',
},
},
feather: { feather: {
name: 'Feather', name: 'Feather',
loading: 'Loading Feather', loading: 'Loading Feather',
@@ -2697,7 +2397,6 @@ const defaultLocales: LocaleTree = {
companies: 'Businesses, jobs and services', companies: 'Businesses, jobs and services',
music: 'Songs, playlists and audio', music: 'Songs, playlists and audio',
picstagram: 'Photo sharing and social feed', picstagram: 'Photo sharing and social feed',
skypic: 'Private snaps, stories and close friends',
feather: 'Short posts and city conversations', feather: 'Short posts and city conversations',
fliptok: 'Short videos and trends', fliptok: 'Short videos and trends',
flare: 'Social posts and live moments', flare: 'Social posts and live moments',
@@ -2733,195 +2432,46 @@ const defaultLocales: LocaleTree = {
'neon-drop': 'Neon block-dropping puzzle', 'neon-drop': 'Neon block-dropping puzzle',
}, },
previews: { previews: {
citywarn: { citywarn: { first: 'Live alerts', second: 'Safety zones', third: 'Incident updates' },
first: 'Live alerts', crypto: { first: 'Synthetic markets', second: 'Portfolio', third: 'Wallet transfers' },
second: 'Safety zones', health: { first: 'Activity rings', second: 'Medical ID', third: 'Health records' },
third: 'Incident updates', 'weazel-news': { first: 'Top stories', second: 'Local reports', third: 'Breaking news' },
}, companies: { first: 'Business directory', second: 'Job requests', third: 'Services' },
crypto: { music: { first: 'Now playing', second: 'Playlists', third: 'Music library' },
first: 'Synthetic markets', picstagram: { first: 'Photo feed', second: 'Stories', third: 'Profiles' },
second: 'Portfolio', feather: { first: 'Short posts', second: 'Following feed', third: 'Conversations' },
third: 'Wallet transfers', fliptok: { first: 'Video feed', second: 'Creator tools', third: 'Trends' },
}, flare: { first: 'Discover people', second: 'Matches', third: 'Live moments' },
health: { calendar: { first: 'Upcoming events', second: 'Day planner', third: 'Reminders' },
first: 'Activity rings', radio: { first: 'Live channels', second: 'Team radio', third: 'Favorites' },
second: 'Medical ID', 'local-pages': { first: 'Local pages', second: 'Reviews', third: 'City discovery' },
third: 'Health records', crewlink: { first: 'Crew roster', second: 'Shared locations', third: 'Coordination' },
}, phone: { first: 'Recent calls', second: 'Contacts', third: 'Voicemail' },
'weazel-news': { messages: { first: 'Conversations', second: 'Media sharing', third: 'Quick replies' },
first: 'Top stories', darkchat: { first: 'Private chats', second: 'Secure groups', third: 'Invitations' },
second: 'Local reports', garage: { first: 'Vehicle list', second: 'Parking locations', third: 'Valet' },
third: 'Breaking news', house: { first: 'Property access', second: 'Residents', third: 'Management' },
}, map: { first: 'Live navigation', second: 'Nearby places', third: 'Route guidance' },
companies: { skyride: { first: 'Ride booking', second: 'Driver tracking', third: 'Trip history' },
first: 'Business directory', banking: { first: 'Account balance', second: 'Transfers', third: 'Transactions' },
second: 'Job requests', billing: { first: 'Open invoices', second: 'Payment requests', third: 'Payment history' },
third: 'Services',
},
music: {
first: 'Now playing',
second: 'Playlists',
third: 'Music library',
},
picstagram: {
first: 'Photo feed',
second: 'Stories',
third: 'Profiles',
},
skypic: {
first: 'Private snaps',
second: 'Friend stories',
third: 'Quick chats',
},
feather: {
first: 'Short posts',
second: 'Following feed',
third: 'Conversations',
},
fliptok: {
first: 'Video feed',
second: 'Creator tools',
third: 'Trends',
},
flare: {
first: 'Discover people',
second: 'Matches',
third: 'Live moments',
},
calendar: {
first: 'Upcoming events',
second: 'Day planner',
third: 'Reminders',
},
radio: {
first: 'Live channels',
second: 'Team radio',
third: 'Favorites',
},
'local-pages': {
first: 'Local pages',
second: 'Reviews',
third: 'City discovery',
},
crewlink: {
first: 'Crew roster',
second: 'Shared locations',
third: 'Coordination',
},
phone: {
first: 'Recent calls',
second: 'Contacts',
third: 'Voicemail',
},
messages: {
first: 'Conversations',
second: 'Media sharing',
third: 'Quick replies',
},
darkchat: {
first: 'Private chats',
second: 'Secure groups',
third: 'Invitations',
},
garage: {
first: 'Vehicle list',
second: 'Parking locations',
third: 'Valet',
},
house: {
first: 'Property access',
second: 'Residents',
third: 'Management',
},
map: {
first: 'Live navigation',
second: 'Nearby places',
third: 'Route guidance',
},
skyride: {
first: 'Ride booking',
second: 'Driver tracking',
third: 'Trip history',
},
banking: {
first: 'Account balance',
second: 'Transfers',
third: 'Transactions',
},
billing: {
first: 'Open invoices',
second: 'Payment requests',
third: 'Payment history',
},
mail: { first: 'Inbox', second: 'Attachments', third: 'Mailboxes' }, mail: { first: 'Inbox', second: 'Attachments', third: 'Mailboxes' },
notes: { first: 'Notes', second: 'Checklists', third: 'Pinned ideas' }, notes: { first: 'Notes', second: 'Checklists', third: 'Pinned ideas' },
memos: { memos: { first: 'Voice recordings', second: 'Playback', third: 'Favorites' },
first: 'Voice recordings', calculator: { first: 'Basic calculation', second: 'Scientific tools', third: 'History' },
second: 'Playback', camera: { first: 'Photo mode', second: 'Video capture', third: 'Zoom controls' },
third: 'Favorites',
},
calculator: {
first: 'Basic calculation',
second: 'Scientific tools',
third: 'History',
},
camera: {
first: 'Photo mode',
second: 'Video capture',
third: 'Zoom controls',
},
clock: { first: 'World clock', second: 'Alarms', third: 'Timers' }, clock: { first: 'World clock', second: 'Alarms', third: 'Timers' },
weather: { weather: { first: 'Current weather', second: 'Hourly forecast', third: 'Seven-day outlook' },
first: 'Current weather', photos: { first: 'Media library', second: 'Albums', third: 'Shared media' },
second: 'Hourly forecast', settings: { first: 'Device controls', second: 'Privacy', third: 'Personalization' },
third: 'Seven-day outlook',
},
photos: {
first: 'Media library',
second: 'Albums',
third: 'Shared media',
},
settings: {
first: 'Device controls',
second: 'Privacy',
third: 'Personalization',
},
snake: { first: 'High score', second: 'Speed', third: 'Classic grid' }, snake: { first: 'High score', second: 'Speed', third: 'Classic grid' },
memory: { memory: { first: 'Matched pairs', second: 'Best time', third: 'Card themes' },
first: 'Matched pairs', 'number-merge': { first: 'Highest tile', second: 'Score', third: 'Strategy grid' },
second: 'Best time', minesweeper: { first: 'Mine counter', second: 'Best time', third: 'Difficulty' },
third: 'Card themes', 'tower-stack': { first: 'Tower height', second: 'Perfect drops', third: 'High score' },
}, 'sky-flappy': { first: 'Flight score', second: 'Best run', third: 'Obstacles' },
'number-merge': { citymarkt: { first: 'Listings', second: 'Categories', third: 'Saved offers' },
first: 'Highest tile', 'neon-drop': { first: 'Lines cleared', second: 'Level', third: 'Neon pieces' },
second: 'Score',
third: 'Strategy grid',
},
minesweeper: {
first: 'Mine counter',
second: 'Best time',
third: 'Difficulty',
},
'tower-stack': {
first: 'Tower height',
second: 'Perfect drops',
third: 'High score',
},
'sky-flappy': {
first: 'Flight score',
second: 'Best run',
third: 'Obstacles',
},
citymarkt: {
first: 'Listings',
second: 'Categories',
third: 'Saved offers',
},
'neon-drop': {
first: 'Lines cleared',
second: 'Level',
third: 'Neon pieces',
},
}, },
search: { search: {
recommended: 'Recommended', recommended: 'Recommended',
@@ -5008,8 +4558,6 @@ const defaultLocales: LocaleTree = {
import_url_not_allowed: 'This link is not from the selected website.', import_url_not_allowed: 'This link is not from the selected website.',
import_url_unavailable: 'The linked media could not be reached.', import_url_unavailable: 'The linked media could not be reached.',
import_size_unavailable: 'The website did not provide the media size.', import_size_unavailable: 'The website did not provide the media size.',
media_in_use:
'This media is still used by SkyPic and cannot be deleted yet.',
not_found: 'The media item no longer exists.', not_found: 'The media item no longer exists.',
profile_photo_required: profile_photo_required:
'This is the last photo on your Flare profile. Add another profile photo before deleting it.', 'This is the last photo on your Flare profile. Add another profile photo before deleting it.',
@@ -5419,10 +4967,8 @@ const defaultLocales: LocaleTree = {
pause: 'Pause', pause: 'Pause',
phone: 'Phone', phone: 'Phone',
phoneStatus: 'Phone status', phoneStatus: 'Phone status',
retry: 'Try Again',
reset: 'Reset', reset: 'Reset',
loading: 'Loading', loading: 'Loading',
loadMore: 'Load More',
search: 'Search', search: 'Search',
save: 'Save', save: 'Save',
send: 'Send', send: 'Send',
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-1
View File
@@ -36,7 +36,6 @@ export type BuiltinPhoneAppId =
| 'flare' | 'flare'
| 'fliptok' | 'fliptok'
| 'picstagram' | 'picstagram'
| 'skypic'
| 'skyride' | 'skyride'
| 'feather' | 'feather'
| 'crewlink' | 'crewlink'
-265
View File
@@ -1,265 +0,0 @@
import type { MediaType, PhoneMedia } from '@/types/media'
export type SkyPicStoryPrivacy = 'everyone' | 'friends'
export type SkyPicDirection = 'received' | 'sent'
export type SkyPicSnapType = 'snap_photo' | 'snap_video'
export type SkyPicFriendshipStatus =
| 'friends'
| 'incoming'
| 'none'
| 'outgoing'
export type SkyPicProfileSummary = {
avatarSeed: number
avatarUrl: string | null
displayName: string
friendshipId?: string | null
friendshipStatus: SkyPicFriendshipStatus
handle: string
id: string
snapScore: number
}
export type SkyPicProfile = SkyPicProfileSummary & {
allowStoryReplies: boolean
avatarMediaId: number | null
bio: string
friendCount: number
showInQuickAdd: boolean
storyPrivacy: SkyPicStoryPrivacy
}
export type SkyPicCreateProfileInput = {
avatarMediaId?: number
avatarSeed?: number
displayName: string
handle: string
}
export type SkyPicUpdateProfileInput = {
allowStoryReplies: boolean
avatarMediaId?: number | null
avatarSeed?: number
bio: string
displayName: string
handle: string
showInQuickAdd: boolean
storyPrivacy: SkyPicStoryPrivacy
}
export type SkyPicFriend = {
bestStreak: number
createdAt: string
friendshipId: string
profile: SkyPicProfileSummary
streakCount: number
}
export type SkyPicFriendRequest = {
createdAt: string
direction: 'incoming' | 'outgoing'
friendshipId: string
profile: SkyPicProfileSummary
}
export type SkyPicConversationLastItem = {
body?: string
createdAt: string
direction: SkyPicDirection
id: string
openedAt: string | null
type: 'snap_photo' | 'snap_video' | 'text'
}
export type SkyPicConversation = {
bestStreak: number
friendshipId: string
lastItem: SkyPicConversationLastItem | null
profile: SkyPicProfileSummary
streakCount: number
unreadCount: number
}
/** Direct snap lists intentionally contain no media URL or editor contents. */
export type SkyPicSnap = {
allowReplay: boolean
/** Present on successful sends so friendship streak UI can reconcile immediately. */
bestStreak?: number
createdAt: string
direction: SkyPicDirection
durationSeconds: number
expiresAt: string
friendshipId: string
id: string
openedAt: string | null
replayedAt: string | null
sender: SkyPicProfileSummary
/** Present on successful sends so friendship streak UI can reconcile immediately. */
streakCount?: number
type: SkyPicSnapType
}
/** Media and editor contents are released only by open/replay callbacks. */
export type SkyPicOpenedSnap = {
allowReplay: boolean
caption: string
durationSeconds: number
expiresAt: string
id: string
mediaType: MediaType
mimeType: string | null
openedAt: string
overlayColor: string
replayedAt: string | null
textOverlay: string
url: string
}
/** Bootstrap returns story metadata only; URLs are released by view-story. */
export type SkyPicStory = {
author: SkyPicProfileSummary
createdAt: string
durationSeconds: number
expiresAt: string
id: string
isOwner: boolean
seen: boolean
viewCount: number
}
export type SkyPicViewedStory = {
author: SkyPicProfileSummary
canReply: boolean
caption: string
durationSeconds: number
expiresAt: string
id: string
mediaType: MediaType
mimeType: string | null
overlayColor: string
textOverlay: string
url: string
viewedAt: string
}
export type SkyPicStoryViewer = SkyPicProfileSummary & {
viewedAt: string
}
export type SkyPicSpotlightReportReason =
| 'dangerous'
| 'harassment'
| 'illegal'
| 'other'
| 'spam'
/** Spotlight is public content, so its media URL is intentionally feed-visible. */
export type SkyPicSpotlight = {
adHeadline: string
author: SkyPicProfileSummary
caption: string
commentCount: number
commentsEnabled: boolean
createdAt: string
expiresAt: string
id: string
isLiked: boolean
isOwner: boolean
isSponsored: boolean
isViewed: boolean
likeCount: number
mimeType: string | null
overlayColor: string
textOverlay: string
url: string
viewCount: number
}
export type SkyPicSpotlightComment = {
author: SkyPicProfileSummary
body: string
createdAt: string
id: string
isOwner: boolean
spotlightId: string
}
export type SkyPicMessageDeliveryStatus = 'delivered' | 'failed' | 'sending'
export type SkyPicMessage = {
body: string
clientId?: string
createdAt: string
deliveryStatus?: SkyPicMessageDeliveryStatus
direction: SkyPicDirection
friendshipId: string
id: string
readAt: string | null
savedAt: string | null
type: 'text'
}
export type SkyPicThread = {
messages: SkyPicMessage[]
snaps: SkyPicSnap[]
}
export type SkyPicBootstrap = {
blockedProfiles: SkyPicProfileSummary[]
conversations: SkyPicConversation[]
friends: SkyPicFriend[]
inbox: SkyPicSnap[]
profile: SkyPicProfile | null
requests: SkyPicFriendRequest[]
stories: SkyPicStory[]
suggestions: SkyPicProfileSummary[]
unreadCount: number
}
export type SkyPicDraftPurpose = 'snap' | 'spotlight' | 'story'
export type SkyPicMediaDraftContext = {
purpose: SkyPicDraftPurpose
recipientIds: string[]
}
export type SkyPicThreadMediaDraftContext = {
body: string
friendshipId: string
pendingMedia: PhoneMedia[]
}
type SkyPicEditorInput = {
caption: string
durationSeconds: number
overlayColor: string
textOverlay: string
}
type SkyPicSingleMediaInput = {
mediaId: number
mediaIds?: never
mediaType: MediaType
}
type SkyPicMultipleMediaInput = {
mediaId?: never
mediaIds: number[]
mediaType?: never
}
export type SkyPicSendSnapInput = SkyPicEditorInput &
(SkyPicSingleMediaInput | SkyPicMultipleMediaInput) & {
allowReplay: boolean
recipientIds: string[]
}
export type SkyPicPublishStoryInput = SkyPicEditorInput & SkyPicSingleMediaInput
export type SkyPicPublishSpotlightInput = SkyPicEditorInput & {
adHeadline: string
commentsEnabled: boolean
isSponsored: boolean
mediaId: number
mediaType: 'video'
}
+6 -1
View File
@@ -10,6 +10,10 @@ import {
PREVIEWABLE_BUILTIN_APP_IDS, PREVIEWABLE_BUILTIN_APP_IDS,
} from '@/utils/appStorePreviews' } from '@/utils/appStorePreviews'
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
}
describe('App Store preview catalog', () => { describe('App Store preview catalog', () => {
it('contains a real captured screenshot for every built-in store app', () => { it('contains a real captured screenshot for every built-in store app', () => {
const storeAppIds = PHONE_APPS.filter( const storeAppIds = PHONE_APPS.filter(
@@ -56,10 +60,11 @@ describe('App Store preview catalog', () => {
] ]
for (const appId of PREVIEWABLE_BUILTIN_APP_IDS) { for (const appId of PREVIEWABLE_BUILTIN_APP_IDS) {
const escapedAppId = escapeRegExp(appId)
for (const source of localeSources) { for (const source of localeSources) {
expect(source).toMatch( expect(source).toMatch(
new RegExp( new RegExp(
`(?:["']${appId}["']\\]?|${appId.replace(/-/g, '\\-')})\\s*[:=]\\s*\\{\\s*first\\s*[:=]`, `(?:["']${escapedAppId}["']\\]?|${escapedAppId})\\s*[:=]\\s*\\{\\s*first\\s*[:=]`,
), ),
) )
} }
-1
View File
@@ -15,7 +15,6 @@ const APP_STORE_PREVIEW_STYLES = {
companies: { accent: '#4a92ff', surface: '#0c1728' }, companies: { accent: '#4a92ff', surface: '#0c1728' },
music: { accent: '#fa3d71', surface: '#240b19' }, music: { accent: '#fa3d71', surface: '#240b19' },
picstagram: { accent: '#e54cff', surface: '#210b27' }, picstagram: { accent: '#e54cff', surface: '#210b27' },
skypic: { accent: '#24c7ff', surface: '#070f2b' },
feather: { accent: '#3c9cff', surface: '#091c2d' }, feather: { accent: '#3c9cff', surface: '#091c2d' },
fliptok: { accent: '#24f0d2', surface: '#071d1b' }, fliptok: { accent: '#24f0d2', surface: '#071d1b' },
flare: { accent: '#ff567f', surface: '#260d18' }, flare: { accent: '#ff567f', surface: '#260d18' },
+11 -3
View File
@@ -112,8 +112,16 @@ describe('LB Phone app bridge', () => {
expect(document).toContain('https://cfx-nui-snake_app/ui/dist/') expect(document).toContain('https://cfx-nui-snake_app/ui/dist/')
expect(document).not.toContain('</script><script>window.injected=true') expect(document).not.toContain('</script><script>window.injected=true')
const runtime = /<script>([\s\S]*?)<\/script>/.exec(document)?.[1] const openingTag = '<script>'
expect(runtime).toBeTruthy() const runtimeStart = document.indexOf(openingTag)
expect(() => new Function(runtime ?? '')).not.toThrow() const runtimeEnd = document.indexOf(
'</script>',
runtimeStart + openingTag.length,
)
expect(runtimeStart).toBeGreaterThanOrEqual(0)
expect(runtimeEnd).toBeGreaterThan(runtimeStart)
const runtime = document.slice(runtimeStart + openingTag.length, runtimeEnd)
expect(() => new Function(runtime)).not.toThrow()
}) })
}) })
-1
View File
@@ -158,7 +158,6 @@ describe('media utilities', () => {
expect(mediaErrorKey('profile_photo_required')).toBe( expect(mediaErrorKey('profile_photo_required')).toBe(
'profile_photo_required', 'profile_photo_required',
) )
expect(mediaErrorKey('media_in_use')).toBe('media_in_use')
expect(mediaErrorKey('private_provider_error')).toBe('request_failed') expect(mediaErrorKey('private_provider_error')).toBe('request_failed')
}) })
}) })
-1
View File
@@ -106,7 +106,6 @@ export function mediaErrorKey(error?: string): string {
'import_url_not_allowed', 'import_url_not_allowed',
'import_url_unavailable', 'import_url_unavailable',
'import_size_unavailable', 'import_size_unavailable',
'media_in_use',
'not_found', 'not_found',
'operation_in_progress', 'operation_in_progress',
'owner_changed', 'owner_changed',
+8
View File
@@ -47,4 +47,12 @@ describe('notes rich text', () => {
'Briefing\nMeet outside.\n• Radio\n• Vest', 'Briefing\nMeet outside.\n• Radio\n• Vest',
) )
}) })
it('keeps encoded markup as literal preview text', () => {
const body = serializeRichNoteBody(
'<p>&lt;script&gt;literal&lt;/script&gt;</p>',
)
expect(noteBodyToPlainText(body)).toBe('<script>literal</script>')
})
}) })
-4
View File
@@ -85,10 +85,6 @@ describe('preferences', () => {
enabled: true, enabled: true,
sounds: true, sounds: true,
}) })
expect(value.settings.notifications.skypic).toEqual({
enabled: true,
sounds: true,
})
expect(value.settings.phoneScale).toBe(110) expect(value.settings.phoneScale).toBe(110)
expect(value.settings.screenBrightness).toBe(64) expect(value.settings.screenBrightness).toBe(64)
expect(value.settings.wallpaper).toBe('ember') expect(value.settings.wallpaper).toBe('ember')
-1
View File
@@ -118,7 +118,6 @@ const DEFAULT_APP_NOTIFICATIONS: Record<
'weazel-news': { enabled: true, sounds: true }, 'weazel-news': { enabled: true, sounds: true },
'local-pages': { enabled: true, sounds: true }, 'local-pages': { enabled: true, sounds: true },
picstagram: { enabled: true, sounds: true }, picstagram: { enabled: true, sounds: true },
skypic: { enabled: true, sounds: true },
fliptok: { enabled: true, sounds: true }, fliptok: { enabled: true, sounds: true },
feather: { enabled: true, sounds: true }, feather: { enabled: true, sounds: true },
crewlink: { enabled: true, sounds: true }, crewlink: { enabled: true, sounds: true },
@@ -1,445 +0,0 @@
import { readFileSync } from 'node:fs'
import { describe, expect, it } from 'vitest'
const viewSource = readFileSync(
new URL('./SkyPicApp.vue', import.meta.url),
'utf8',
)
const storeSource = readFileSync(
new URL('../../stores/skypic.ts', import.meta.url),
'utf8',
)
const typeSource = readFileSync(
new URL('../../types/skypic.ts', import.meta.url),
'utf8',
)
describe('SkyPic frontend contract', () => {
it('uses Sky UI and exposes the camera-first five-tab shell', () => {
expect(viewSource).toContain("from '@/ui'")
expect(viewSource).not.toContain("from 'konsta/vue'")
expect(viewSource).toContain("const activeTab = ref<Tab>('camera')")
expect(viewSource).toContain(
"type Tab = 'camera' | 'chats' | 'friends' | 'spotlight' | 'stories'",
)
expect(viewSource).toContain("activeTab === 'camera'")
expect(viewSource).toContain("activeTab === 'chats'")
expect(viewSource).toContain("activeTab === 'stories'")
expect(viewSource).toContain("activeTab === 'friends'")
expect(viewSource).toContain("activeTab === 'spotlight'")
expect(viewSource.match(/<SkyTabButton/g)).toHaveLength(5)
})
it('keeps every bottom tab and app surface theme-monochrome', () => {
expect(viewSource).toContain('phone.isDarkMode')
expect(viewSource).toContain("'skypic-app--player-dark': phone.isDarkMode")
expect(viewSource).toContain(
"'skypic-app--player-light': !phone.isDarkMode",
)
expect(viewSource).toContain(
'class="sp-tab sp-tab--camera sp-tab--monochrome"',
)
expect(viewSource.match(/class="sp-tab sp-tab--monochrome"/g)).toHaveLength(
4,
)
expect(viewSource).not.toContain('color: var(--sp-camera-blue) !important')
expect(viewSource).toContain('--sp-tab-monochrome: #000000')
expect(viewSource).toContain('--sp-tab-monochrome: #ffffff')
expect(viewSource).toContain('color: var(--sp-tab-monochrome) !important')
const cameraCss = viewSource
.split('.sp-camera-screen {')[1]
?.split('.sp-camera-header')[0]
expect(cameraCss).toBeTruthy()
expect(cameraCss).toContain('background: #000')
expect(cameraCss).not.toContain('255, 91, 189')
expect(viewSource).toContain('rgba(18, 18, 18, 0.92)')
expect(viewSource).not.toContain('var(--sp-cyan)')
})
it('uses the reference-led Sky UI hierarchy with Spotlight and no camera filters', () => {
expect(viewSource.match(/class="sp-camera-header__actions"/g)).toHaveLength(
2,
)
expect(viewSource.match(/class="sp-camera-header__glass"/g)).toHaveLength(3)
expect(viewSource).toContain('class="sp-camera-viewfinder"')
expect(viewSource).toContain('class="sp-camera-dock"')
const cameraMarkup = viewSource
.split('v-if="activeTab === \'camera\'"')[1]
?.split('<template v-else-if="activeTab === \'chats\'">')[0]
expect(cameraMarkup).toContain('<SkyGlass')
expect(cameraMarkup).toContain('class="sp-shutter"')
expect(viewSource).toContain('class="sp-chat-list"')
expect(viewSource).toContain('class="sp-chat-row')
expect(viewSource).toContain('class="sp-story-rail"')
expect(viewSource).toContain('class="sp-story-grid"')
expect(viewSource).toContain('class="sp-discovery-grid"')
expect(viewSource).toContain('width: calc(100% - (var(--sky-space-3) * 2))')
expect(viewSource).toContain('class="sp-bottom-nav"')
expect(viewSource).not.toContain('sp-camera-filter')
expect(viewSource).toContain('class="sp-spotlight-player"')
expect(
viewSource.match(/class="sp-spotlight-header__actions/g),
).toHaveLength(2)
expect(
viewSource.match(/class="sp-spotlight-header__glass"/g),
).toHaveLength(3)
expect(viewSource).toContain(
'height: calc(var(--sky-safe-area-top) + var(--sky-navbar-height))',
)
expect(viewSource).toContain(
'padding: var(--sky-safe-area-top) var(--sky-page-gutter)',
)
expect(viewSource).toContain('min-height: var(--sky-navbar-height)')
expect(viewSource).toContain("t('spotlight.sponsored')")
expect(viewSource).toContain('store.publishSpotlight')
expect(viewSource).not.toContain('href=')
})
it('builds story discovery only from safe metadata until view-story releases media', () => {
const storyFeedBlock = viewSource
.split('<section class="sp-story-feed">')[1]
?.split('</section>')[0]
expect(viewSource).toContain('const friendStoryRail = computed(')
expect(storyFeedBlock).toBeTruthy()
expect(storyFeedBlock).toContain('v-for="story in store.stories"')
expect(storyFeedBlock).toContain('story.author.avatarUrl')
expect(storyFeedBlock).toContain('story.durationSeconds')
expect(storyFeedBlock).not.toContain('story.url')
expect(storyFeedBlock).not.toContain('store.viewedStory.url')
})
it('renders every visible streak as an accessible Flame icon', () => {
expect(viewSource.match(/<Flame/g)?.length).toBeGreaterThanOrEqual(4)
expect(viewSource).toContain("t('profile.streaks') +")
expect(viewSource).toContain('class="sp-navbar-streak"')
expect(viewSource).toContain('class="sp-profile-streak-stat"')
expect(viewSource).not.toContain('🔥')
})
it('keeps existing Snap and Story viewers full-bleed with ordered overlays', () => {
expect(viewSource).toContain(
'class="sp-media-viewer sp-media-viewer--snap"',
)
expect(viewSource).toContain(
'class="sp-media-viewer sp-media-viewer--story"',
)
expect(viewSource).toContain('object-fit: cover')
expect(viewSource).toContain('.sp-media-viewer::before')
expect(viewSource).toContain('.sp-media-viewer::after')
})
it('uses the shared Camera and Gallery media handoff with a bounded draft', () => {
expect(viewSource).toContain('mediaPicker.begin(')
expect(viewSource).toContain("'skypic-draft'")
expect(viewSource).toContain('`/apps/skypic?compose=${purpose}`')
expect(viewSource).toContain(
"mediaPicker.consumeMany<SkyPicMediaDraftContext>('skypic-draft')",
)
expect(viewSource).toContain('path: `/apps/${source}`')
expect(viewSource).toContain('query: { mediaAttachment: mediaType }')
expect(viewSource).toContain("type MediaSource = 'camera' | 'photos'")
})
it('supports a bounded multi-photo thread handoff with preview controls', () => {
expect(viewSource).toContain('const MAX_THREAD_ATTACHMENTS = 10')
expect(viewSource).toContain("'skypic-thread-media'")
expect(viewSource).toContain(
'mediaPicker.consumeMany<SkyPicThreadMediaDraftContext>',
)
expect(viewSource).toContain('pendingThreadMedia')
expect(viewSource).toContain('mediaIds: queuedMedia.map')
expect(viewSource).toContain('removeThreadMedia(media.id)')
expect(viewSource).toContain('moveThreadMedia(index, -1)')
expect(viewSource).toContain('moveThreadMedia(index, 1)')
expect(viewSource).toContain("openThreadMedia('photos', 'photo')")
expect(viewSource).toContain("openThreadMedia('camera', 'photo')")
expect(viewSource).toContain("openThreadMedia('photos', 'video')")
expect(viewSource).toContain('openThreadEmojiPicker')
expect(typeSource).toContain('SkyPicThreadMediaDraftContext')
expect(typeSource).toContain('mediaIds: number[]')
})
it('aligns chat ownership and keeps the story reply action inside the viewer', () => {
expect(viewSource).toContain(
`:type="entry.value.direction === 'sent' ? 'sent' : 'received'"`,
)
const threadLayoutCss = viewSource
.split('.sp-thread :deep(.sky-messages) {')[1]
?.split('.sp-thread :deep(.sky-message__footer)')[0]
expect(threadLayoutCss).toContain('display: flex')
expect(threadLayoutCss).toContain('flex-direction: column')
const sentSnapCss = viewSource
.split('.sp-thread-snap--sent {')[1]
?.split('.sp-thread-snap > span')[0]
expect(sentSnapCss).toContain('align-self: flex-end')
const storyReplyCss = viewSource
.split('.sp-story-reply {')[1]
?.split('.sp-media-viewer__owner-actions button')[0]
expect(storyReplyCss).toContain('width: auto')
expect(storyReplyCss).toContain('right: var(--sky-page-gutter)')
expect(storyReplyCss).toContain('left: var(--sky-page-gutter)')
})
it('uses app-local authentication and exposes safe account controls', () => {
expect(viewSource).toContain('useAppAuthStore()')
expect(viewSource).toContain("appAuth.isSignedIn('skypic')")
expect(viewSource).toContain("appAuth.signIn('skypic', account.email)")
expect(viewSource).toContain("appAuth.signOut('skypic')")
expect(viewSource).toContain('<AppProfileAuth')
expect(viewSource).toContain('moving-mode-highlight')
expect(viewSource).toContain(':login-mode-label=')
expect(viewSource).toContain(':register-mode-label=')
expect(viewSource).toContain('<AccountLogoutDialog')
expect(viewSource).toContain('app-id="skypic"')
expect(viewSource).toContain('deleteSkyPicAccount')
expect(viewSource).toContain('store.resetSession()')
expect(storeSource).toContain("'skypic:delete-account'")
expect(storeSource).toContain('confirmed: true')
})
it('uses the production SkyPic handle rule for login and registration', () => {
expect(viewSource).toContain('isValidSkyPicHandle(authHandle.value)')
expect(viewSource).toContain(
'const handle = normalizeSkyPicHandle(onboarding.handle)',
)
expect(storeSource).toContain('/^[a-z0-9._]+$/')
})
it('reacts to confirmed remote account deletion with a clean register state', () => {
expect(viewSource).toContain('() => store.profileAbsentRevision')
const resetBlock = viewSource
.split('function resetAccountUiState(accountExists: boolean): void {')[1]
?.split('function handleLoggedOut')[0]
expect(resetBlock).toContain(
"authMode.value = accountExists ? 'login' : 'register'",
)
expect(resetBlock).toContain('deleteAccountDialogOpen.value = false')
expect(resetBlock).toContain('profileEditing.value = false')
expect(resetBlock).toContain('pendingThreadMedia.value = []')
expect(resetBlock).toContain('storyViewerSheetOpen.value = false')
expect(resetBlock).toContain("storyViewerSheetStoryId.value = ''")
expect(resetBlock).toContain("storyReply.value = ''")
expect(resetBlock).toContain('storyNavigationRequest += 1')
expect(resetBlock).toContain('clearStoryTimer()')
expect(resetBlock).toContain('store.clearViewedStory()')
expect(resetBlock).toContain("activeTab.value = 'camera'")
})
it('clears private SkyPic state while the local app session is signed out', () => {
const logoutBlock = viewSource
.split('function handleLoggedOut(): void {')[1]
?.split('async function deleteSkyPicAccount')[0]
const bootstrapBlock = viewSource
.split('async function bootstrapApp(): Promise<void> {')[1]
?.split('onMounted(')[0]
expect(logoutBlock).toContain('store.resetSession()')
expect(logoutBlock).not.toContain('bootstrapApp()')
expect(bootstrapBlock).toContain("!appAuth.isSignedIn('skypic')")
expect(bootstrapBlock).toContain('store.resetSession()')
expect(viewSource).toContain(
'hasSkyPicAccount.value = Boolean(store.profile)',
)
})
it('renders local registration guidance without bootstrapping an absent Sky account', () => {
const bootstrapBlock = viewSource
.split('async function bootstrapApp(): Promise<void> {')[1]
?.split('onMounted(')[0]
const noAccountGuard = bootstrapBlock?.indexOf('if (!account.email)') ?? -1
const serverBootstrap = bootstrapBlock?.indexOf(
'const loaded = await store.bootstrap()',
)
expect(noAccountGuard).toBeGreaterThanOrEqual(0)
expect(noAccountGuard).toBeLessThan(serverBootstrap ?? -1)
expect(bootstrapBlock).toContain('resetAccountUiState(false)')
expect(bootstrapBlock).toContain('bootstrapped.value = true')
})
it('keeps profile editing parse-safe and composer inputs canonical', () => {
expect(viewSource).toContain('function toggleProfileEditor(): void')
expect(viewSource.match(/@click="toggleProfileEditor"/g)).toHaveLength(3)
expect(viewSource).toContain('const MAX_CAPTION_LENGTH = 160')
expect(viewSource).toContain(':maxlength="MAX_CAPTION_LENGTH"')
expect(viewSource).toContain('.slice(0, MAX_CAPTION_LENGTH)')
expect(viewSource).not.toContain('maxlength="240"')
expect(viewSource).toContain('const MAX_TEXT_OVERLAY_LENGTH = 160')
expect(viewSource).toContain(':maxlength="MAX_TEXT_OVERLAY_LENGTH"')
expect(storeSource).toContain('MAX_TEXT_OVERLAY_CHARACTERS = 160')
expect(viewSource).toContain(
'avatarSeed: Math.floor(Math.random() * 360) + 1',
)
})
it('keeps direct and story secrets behind explicit release callbacks', () => {
const snapBlock = typeSource.match(
/export type SkyPicSnap = \{([\s\S]*?)\n\}/,
)?.[1]
const openedSnapBlock = typeSource.match(
/export type SkyPicOpenedSnap = \{([\s\S]*?)\n\}/,
)?.[1]
const storyBlock = typeSource.match(
/export type SkyPicStory = \{([\s\S]*?)\n\}/,
)?.[1]
const viewedStoryBlock = typeSource.match(
/export type SkyPicViewedStory = \{([\s\S]*?)\n\}/,
)?.[1]
expect(snapBlock).toBeTruthy()
expect(snapBlock).not.toMatch(/\burl\b|caption|textOverlay|overlayColor/)
expect(openedSnapBlock).toMatch(/\burl: string\b/)
expect(openedSnapBlock).toMatch(/caption: string/)
expect(openedSnapBlock).toMatch(/textOverlay: string/)
expect(openedSnapBlock).toMatch(/overlayColor: string/)
expect(storyBlock).not.toMatch(/\burl\b|caption|textOverlay|overlayColor/)
expect(viewedStoryBlock).toMatch(/\burl: string\b/)
expect(storeSource).toContain("'skypic:open-snap'")
expect(storeSource).toContain("'skypic:replay-snap'")
expect(storeSource).toContain("'skypic:view-story'")
expect(viewSource).toContain('store.clearOpenedSnap()')
expect(viewSource).toContain('store.clearViewedStory()')
})
it('implements all agreed callback names exactly', () => {
const callbacks = [
'skypic:bootstrap',
'skypic:create-profile',
'skypic:delete-account',
'skypic:update-profile',
'skypic:search',
'skypic:add-friend',
'skypic:respond-friend',
'skypic:remove-friend',
'skypic:block',
'skypic:send-snap',
'skypic:open-snap',
'skypic:replay-snap',
'skypic:publish-story',
'skypic:stories',
'skypic:view-story',
'skypic:story-viewers',
'skypic:remove-story',
'skypic:thread',
'skypic:send-message',
'skypic:mark-thread',
'skypic:save-message',
'skypic:delete-message',
]
callbacks.forEach((callback) =>
expect(storeSource).toContain("'" + callback + "'"),
)
})
it('reacts to query-only notification and media-return deep links', () => {
expect(viewSource).toContain('() => route.query')
expect(viewSource).toContain('{ deep: true }')
expect(viewSource).toContain('query.compose')
expect(viewSource).toContain('query.profileId')
expect(viewSource).toContain('query.friendship')
expect(viewSource).toContain('query.snap')
expect(viewSource).toContain('query.story')
expect(viewSource).toContain('conversationForProfile(profileId)')
expect(viewSource).toContain(
'if (!requestedFriendship && store.activeFriendshipId)',
)
expect(viewSource).toContain("chatBody.value = ''")
})
it('only opens incoming direct snaps', () => {
const canOpenBlock = viewSource.match(
/function snapCanOpen\(snap: SkyPicSnap\): boolean \{([\s\S]*?)\n\}/,
)?.[1]
expect(canOpenBlock).toContain("snap.direction === 'received'")
expect(canOpenBlock).toContain('snap.allowReplay')
})
it('implements server-authorized story replies', () => {
expect(typeSource).toContain('canReply: boolean')
expect(viewSource).toContain('store.viewedStory.canReply')
expect(viewSource).toContain('stories.replyPlaceholder')
expect(viewSource).toContain(
'store.sendMessage(friendshipId, body, story.id)',
)
expect(viewSource).toContain('@focus="pauseStoryCountdown"')
expect(viewSource).toContain('@blur="resumeStoryCountdown"')
expect(storeSource).toContain('...(storyId ? { storyId } : {})')
})
it('renders outgoing requests with a cancellable relationship action', () => {
expect(viewSource).toContain('store.outgoingRequests')
expect(viewSource).toContain('cancelFriendRequest(request)')
expect(viewSource).toContain("'friends.cancelRequest'")
expect(storeSource).toContain("'skypic:remove-friend'")
})
it('has timer cleanup, conditional scroll owners and accessible controls', () => {
expect(viewSource).toContain('beginCountdown(')
expect(viewSource).toContain('clearSnapTimer()')
expect(viewSource).toContain('clearStoryTimer()')
expect(viewSource).toContain('onBeforeUnmount(')
expect(viewSource).toContain('<SkyScrollArea')
expect(viewSource).toContain('with-tabbar')
expect(viewSource).toContain('var(--sky-touch-target)')
expect(viewSource).toContain(':focus-visible')
expect(viewSource).toContain('@media (prefers-reduced-motion: reduce)')
expect(viewSource).toContain('aria-modal="true"')
})
it('starts viewer timers only after media readiness and closes sheets safely', () => {
expect(viewSource).toContain(
"prepareViewerMedia('snap', snap.durationSeconds)",
)
expect(viewSource).toContain(
"prepareViewerMedia('story', story.durationSeconds)",
)
expect(viewSource).toContain("handleViewerMediaReady('snap')")
expect(viewSource).toContain("handleViewerMediaReady('story')")
expect(viewSource).toContain("handleViewerVideoCanPlay('snap', $event)")
expect(viewSource).toContain("handleViewerVideoCanPlay('story', $event)")
expect(viewSource).toContain('await video.play()')
expect(viewSource).toContain('mediaLoading.value = true')
expect(viewSource).toContain('mediaError.value = true')
expect(viewSource).toContain('pauseStoryCountdown()')
expect(viewSource).toContain('storyViewerSheetOpen.value = false')
})
it('guards stale tab, story and viewer-sheet requests', () => {
expect(viewSource).toContain(
'navigationRequest !== threadNavigationRequest',
)
expect(viewSource).toContain('activeTab.value !== next')
expect(viewSource).toContain(
'if (!appMounted || store.storyViewing) return',
)
expect(viewSource).toContain('const requestId = ++storyNavigationRequest')
expect(viewSource).toContain('const requestId = ++storyViewerRequest')
expect(viewSource).toContain('store.viewedStory?.id === story.id')
expect(storeSource).toContain("error: 'story_view_in_progress'")
expect(storeSource).toContain('requestId !== storyRequest')
expect(storeSource).toContain('requestId !== storyViewersRequest')
expect(viewSource).toContain('if (!appMounted) return')
expect(viewSource).toContain('threadNavigationRequest += 1')
expect(viewSource).toContain('store.closeThread()')
})
it('bounds discovery search and clears failed results', () => {
expect(viewSource).toContain('const MAX_SEARCH_CHARACTERS = 64')
expect(viewSource).toContain("slice(0, MAX_SEARCH_CHARACTERS).join('')")
expect(viewSource).toContain('@update:model-value="updateSearchQuery"')
expect(viewSource).toContain('notify(errorText(store.error ?? undefined))')
expect(storeSource).toContain('searchResults.value = []')
})
it('deduplicates concurrent account bootstraps', () => {
expect(storeSource).toContain(
'bootstrapInFlight?.session === requestSession',
)
expect(storeSource).toContain('return bootstrapInFlight.promise')
expect(storeSource).toContain(
'bootstrapInFlight = { promise, session: requestSession, token }',
)
})
})
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-53
View File
@@ -489,59 +489,6 @@ Config.Picstagram = {
AdminGroups = { "admin" }, AdminGroups = { "admin" },
} }
Config.SkyPic = {
PageSize = 30,
ThreadPageSize = 200,
InboxPageSize = 100,
SuggestionLimit = 20,
SearchLimit = 30,
HandleMinLength = 3,
HandleMaxLength = 24,
DisplayNameMaxLength = 40,
BioMaxLength = 160,
MessageMaxLength = 2000,
CaptionMaxLength = 160,
OverlayTextMaxLength = 160,
MinimumViewSeconds = 1,
MaximumViewSeconds = 10,
MaximumSnapRecipients = 20,
MaximumMediaPerSend = 10,
MaximumSnapMessagesPerSend = 40,
MaximumFriends = 500,
MaximumPendingRequests = 100,
MaximumActiveStories = 50,
MaximumActiveSpotlights = 20,
MaximumActiveSponsoredSpotlights = 3,
UnopenedSnapLifetimeSeconds = 30 * 24 * 60 * 60,
ReplayWindowSeconds = 5 * 60,
TextAfterReadLifetimeSeconds = 24 * 60 * 60,
StoryLifetimeSeconds = 24 * 60 * 60,
SpotlightLifetimeSeconds = 30 * 24 * 60 * 60,
SponsoredSpotlightLifetimeSeconds = 7 * 24 * 60 * 60,
SpotlightPageSize = 12,
SpotlightCommentPageSize = 50,
SpotlightCommentMaxLength = 500,
SpotlightAdHeadlineMaxLength = 80,
AllowSponsoredSpotlights = true,
SpotlightReportReasons = { "spam", "harassment", "dangerous", "illegal", "other" },
CleanupIntervalSeconds = 45,
ProfileActionsPerMinute = 10,
ReadActionsPerMinute = 120,
SearchActionsPerMinute = 30,
FriendActionsPerMinute = 30,
MessagesPerMinute = 30,
SnapsPerMinute = 20,
SnapRecipientsPerMinute = 120,
OpensPerMinute = 120,
StoriesPerMinute = 6,
StoryViewsPerMinute = 120,
SpotlightsPerMinute = 4,
SpotlightViewsPerMinute = 120,
SpotlightReactionsPerMinute = 120,
SpotlightCommentsPerMinute = 20,
SpotlightReportsPerMinute = 10,
}
Config.Feather = { Config.Feather = {
PageSize = 20, PageSize = 20,
TextMaxLength = 360, TextMaxLength = 360,
+3 -174
View File
@@ -128,8 +128,8 @@ Locales["de"] = {
}, },
Common = { Common = {
add = "Hinzufügen", back = "Zurück", cancel = "Abbrechen", clear = "Leeren", close = "Schließen", continue = "Weiter", delete = "Löschen", done = "Fertig", edit = "Bearbeiten", home = "Home", loading = "Wird geladen", pause = "Pause", use = "Verwenden", add = "Hinzufügen", back = "Zurück", cancel = "Abbrechen", clear = "Leeren", close = "Schließen", continue = "Weiter", delete = "Löschen", done = "Fertig", edit = "Bearbeiten", home = "Home", loading = "Wird geladen", pause = "Pause", use = "Verwenden",
phone = "Telefon", phoneStatus = "Telefonstatus", reset = "Zurücksetzen", retry = "Erneut versuchen", phone = "Telefon", phoneStatus = "Telefonstatus", reset = "Zurücksetzen",
loadMore = "Mehr laden", save = "Speichern", search = "Suchen", send = "Senden", start = "Starten", stop = "Stoppen", save = "Speichern", search = "Suchen", send = "Senden", start = "Starten", stop = "Stoppen",
signOut = "Abmelden", signingOut = "Wird abgemeldet...", signOutTitle = "Von {app} abmelden?", signOut = "Abmelden", signingOut = "Wird abgemeldet...", signOutTitle = "Von {app} abmelden?",
signOutBody = "Du wirst nur von {app} abgemeldet. Deine anderen iFruit-Apps bleiben angemeldet.", signOutFailed = "Abmelden fehlgeschlagen. Versuche es erneut.", signOutBody = "Du wirst nur von {app} abgemeldet. Deine anderen iFruit-Apps bleiben angemeldet.", signOutFailed = "Abmelden fehlgeschlagen. Versuche es erneut.",
appAuth = { appAuth = {
@@ -530,175 +530,6 @@ Locales["de"] = {
default = "Picstagram konnte die Anfrage nicht abschließen." default = "Picstagram konnte die Anfrage nicht abschließen."
}, },
}, },
skypic = {
name = "SkyPic",
loading = "SkyPic wird geladen",
navigation = "SkyPic-Navigation",
time = {
seconds = "vor {count} Sek.", minutes = "vor {count} Min.",
hours = "vor {count} Std.", days = "vor {count} Tagen",
},
onboarding = {
title = "Willkommen bei SkyPic", eyebrow = "Dein privates Kameranetzwerk",
heading = "Teile den Moment",
body = "Erstelle ein Profil für private Snaps, Stories mit Freunden, Spotlight-Videos und schnelle Chats.",
displayName = "Anzeigename", displayNamePlaceholder = "Dein Name",
handle = "Handle", handlePlaceholder = "dein.handle",
accountBound = "Dein SkyPic-Profil bleibt mit diesem Sky-Cloud-Konto verbunden.",
create = "Profil erstellen", creating = "Wird erstellt...",
},
auth = {
eyebrow = "Dein SkyPic-Account", title = "Willkommen zurück",
body = "Fahre mit deinem iFruit-Account fort, um auf dein SkyPic-Profil, deine Snaps und Chats zuzugreifen.",
login = "Weiter zu SkyPic", loggingIn = "Wird angemeldet...",
noAccount = "Neu bei SkyPic? Erstelle einen iFruit-Account, um loszulegen.",
},
camera = {
eyebrow = "Sky-Kamera", title = "Aufnehmen",
body = "Nimm ein Foto oder Video auf und teile es für nur wenige Sekunden.",
snap = "Snap", story = "Story", photo = "Foto", video = "Video",
gallery = "Galerie öffnen", capturePhoto = "Foto aufnehmen", captureVideo = "Video aufnehmen",
snapHint = "Sende es privat an einen oder mehrere Freunde.",
storyHint = "Teile es 24 Stunden lang in deiner Story.",
},
composer = {
snapTitle = "Neuer Snap", storyTitle = "Neue Story", spotlightTitle = "Neuer Spotlight",
send = "Senden", addStory = "Zur Story hinzufügen", publishSpotlight = "Veröffentlichen",
caption = "Bildunterschrift", captionPlaceholder = "Bildunterschrift hinzufügen...",
textOverlay = "Text im Bild", textPlaceholder = "Schreib etwas auf den Moment...",
color = "Textfarbe", duration = "Anzeigedauer", seconds = "{count} Sekunden",
replay = "Eine Wiederholung erlauben",
replayBody = "Empfänger dürfen diesen Snap ein weiteres Mal öffnen.",
recipients = "Empfänger", recipientsHint = "Wähle einen oder mehrere Freunde.", recipientLimit = "Wähle bis zu {count} Freunde aus.",
selectedCount = "{count} ausgewählt", noFriends = "Füge vor dem Senden einen Freund hinzu.",
sponsored = "Gesponserter Beitrag", sponsoredBody = "Kennzeichnet diesen Spotlight eindeutig als Werbung.",
adHeadline = "Werbeheadline", adHeadlinePlaceholder = "Was sollen andere entdecken?",
allowComments = "Kommentare erlauben", allowCommentsBody = "Andere können diesen Spotlight kommentieren.",
changeMedia = "Foto oder Video ändern", sent = "Snap gesendet.", storyPublished = "Story veröffentlicht.",
spotlightPublished = "Spotlight veröffentlicht.",
},
tabs = { camera = "Kamera", chats = "Chats", stories = "Stories", friends = "Freunde", spotlight = "Spotlight" },
spotlight = {
title = "Spotlight", add = "Spotlight erstellen", empty = "Noch keine Spotlight-Videos",
emptyBody = "Nimm das erste Kurzvideo für die SkyPic-Community auf.",
videoBy = "Spotlight-Video von {name}", sponsored = "Gesponsert", viewProfile = "Creator-Profil ansehen",
like = "Gefällt mir", comments = "Kommentare", views = "Aufrufe", noComments = "Noch keine Kommentare",
commentPlaceholder = "Kommentar schreiben...", sendComment = "Senden", commentsDisabled = "Kommentare sind für diesen Spotlight deaktiviert.",
deleteComment = "Kommentar löschen", delete = "Spotlight löschen", deleted = "Spotlight gelöscht.",
report = "Spotlight melden", reportBody = "Warum soll dieser Spotlight überprüft werden?",
submitReport = "Meldung senden", reported = "Spotlight gemeldet.",
navigation = "Spotlight-Navigation", previous = "Vorheriger Spotlight", next = "Nächster Spotlight",
reportReasons = {
spam = "Spam oder irreführend", harassment = "Belästigung", dangerous = "Gefährlicher Inhalt",
illegal = "Illegaler Inhalt", other = "Sonstiges",
},
},
chats = {
title = "Chats", incoming = "Neue Snaps", noSnaps = "Keine ungeöffneten Snaps",
conversations = "Unterhaltungen", noConversations = "Deine Unterhaltungen erscheinen hier.",
start = "Unterhaltung beginnen", message = "Nachricht", threadPlaceholder = "Nachricht schreiben...", messageLimit = "Nachrichten dürfen bis zu {count} Zeichen enthalten.",
saved = "Im Chat gespeichert", save = "Speichern", unsave = "Nicht mehr speichern", delete = "Löschen",
sendSnap = "Snap senden", moreActions = "Weitere Aktionen",
attachPhoto = "Fotos anhängen", takePhoto = "Foto aufnehmen", emoji = "Emoji", attachVideo = "Video anhängen",
attachmentPreview = "Ausgewählte Anhänge", removeAttachment = "Anhang {number} entfernen",
moveAttachmentEarlier = "Anhang {number} nach vorne verschieben", moveAttachmentLater = "Anhang {number} nach hinten verschieben",
attachmentLimit = "Du kannst bis zu {count} Fotos anhängen.",
sendingAttachments = "Fotos werden gesendet...", photoAttachmentsSent = "Fotos gesendet.",
sending = "Wird gesendet...", failed = "Nicht zugestellt",
},
snaps = {
newVideo = "Neuer Video-Snap", newPhoto = "Neuer Foto-Snap", replayed = "Wiederholt",
opened = "Geöffnet", video = "Video-Snap", photo = "Foto-Snap", replay = "Snap wiederholen",
},
stories = {
title = "Stories", add = "Story hinzufügen", yours = "Deine Story", friends = "Freunde",
emptyTitle = "Gerade keine Stories",
emptyBody = "Stories deiner Freunde erscheinen hier für 24 Stunden.",
views = "{count} Aufrufe", viewers = "Zuschauer", noViewers = "Noch keine Aufrufe",
replyPlaceholder = "Auf diese Story antworten...", replySent = "Antwort gesendet.", replyLimit = "Antworten dürfen bis zu {count} Zeichen enthalten.",
delete = "Story löschen", deleted = "Story gelöscht.", seen = "Gesehen", unseen = "Neu",
},
friends = {
title = "Freunde", searchPlaceholder = "Name oder @Handle suchen",
searchResults = "Suchergebnisse", score = "{count} Punkte",
requests = "Freundschaftsanfragen", sentRequests = "Gesendete Anfragen", accept = "Annehmen", decline = "Ablehnen",
quickAdd = "Schnell hinzufügen", add = "Hinzufügen", pending = "Ausstehend", cancelRequest = "Anfrage zurückziehen", all = "Deine Freunde",
empty = "Füge Freunde hinzu, um Snaps zu senden.", remove = "Freund entfernen", block = "Blockieren", blockedProfiles = "Blockierte Profile", unblock = "Blockierung aufheben",
chat = "Chat", sendSnap = "Snap senden", respond = "Antworten", friends = "Freunde",
requestSent = "Freundschaftsanfrage an {name} gesendet.", requestCanceled = "Freundschaftsanfrage an {name} zurückgezogen.",
removed = "{name} wurde aus deinen Freunden entfernt.", blocked = "{name} wurde blockiert.", unblocked = "{name} wurde entsperrt.",
},
profile = {
title = "Profil", edit = "Profil bearbeiten", save = "Speichern", cancel = "Abbrechen",
score = "Snap-Score", streaks = "Serien", friends = "Freunde", bio = "Bio",
bioPlaceholder = "Erzähl deinen Freunden ein wenig über dich...",
storyPrivacy = "Story-Sichtbarkeit", privacyFriends = "Nur Freunde", privacyEveryone = "Alle",
allowStoryReplies = "Story-Antworten erlauben",
allowStoryRepliesBody = "Freunde können im Chat auf deine Stories antworten.",
showInQuickAdd = "In Schnell hinzufügen anzeigen",
showInQuickAddBody = "Andere Profile dürfen dich als Vorschlag entdecken.",
saved = "Profil aktualisiert.",
account = "Account", logout = "Abmelden", logoutTitle = "Von SkyPic abmelden?",
logoutBody = "Du wirst nur von SkyPic abgemeldet. Dein Profil, deine Snaps und Chats bleiben verfügbar.",
loggingOut = "Wird abgemeldet...", deleteAccount = "SkyPic-Account löschen",
deleteAccountTitle = "Deinen SkyPic-Account löschen?",
deleteAccountBody = "Dein SkyPic-Profil, deine Freunde, Snaps, Stories, Spotlights, Kommentare und Chats werden dauerhaft gelöscht. Dein iFruit-Account und deine Fotomediathek bleiben verfügbar.",
deletingAccount = "Account wird gelöscht...",
},
viewer = { close = "Schließen", timeLeft = "{count}s" },
notifications = {
friend_request = "{actor} hat dir eine Freundschaftsanfrage gesendet.",
friend_accepted = "{actor} hat deine Freundschaftsanfrage angenommen.",
snap = "{actor} hat dir einen Snap gesendet.", message = "{actor} hat dir eine Nachricht gesendet.",
story_reply = "{actor} hat auf deine Story geantwortet.", snap_opened = "{actor} hat deinen Snap geöffnet.",
default = "Du hast neue SkyPic-Aktivität.",
},
errors = {
profile_required = "Erstelle zuerst dein SkyPic-Profil.",
profile_exists = "Dieses Konto hat bereits ein SkyPic-Profil.",
invalid_handle = "Verwende 3-24 Buchstaben, Zahlen, Punkte oder Unterstriche.",
handle_taken = "Dieser Handle ist bereits vergeben.",
invalid_display_name = "Gib einen Anzeigenamen ein.", invalid_bio = "Deine Bio ist zu lang.",
invalid_avatar = "Wähle ein gültiges Profilfoto.", invalid_avatar_seed = "Wähle einen gültigen Avatar.",
invalid_privacy = "Wähle eine gültige Story-Sichtbarkeit.",
invalid_request = "Diese Freundschaftsanfrage ist ungültig.",
profile_not_found = "Dieses SkyPic-Profil ist nicht verfügbar.", blocked = "Dieses Profil ist blockiert.",
friendship_not_found = "Diese Freundschaft ist nicht verfügbar.",
friend_request_exists = "Eine Freundschaftsanfrage besteht bereits.",
friend_limit_reached = "Deine Freundesliste ist voll.",
request_limit_reached = "Zu viele offene Freundschaftsanfragen.",
invalid_recipients = "Wähle mindestens einen aktuellen Freund.",
invalid_media = "Wähle Medien von diesem Telefon.",
invalid_media_type = "Dieser Medientyp wird nicht unterstützt.",
invalid_duration = "Wähle eine Anzeigedauer von 1 bis 10 Sekunden.",
invalid_caption = "Diese Bildunterschrift ist zu lang.",
invalid_overlay = "Dieser eingeblendete Text ist zu lang.",
invalid_color = "Wähle eine gültige Textfarbe.",
snap_unavailable = "Dieser Snap ist nicht mehr verfügbar.",
replay_unavailable = "Dieser Snap kann nicht noch einmal wiederholt werden.",
message_empty = "Schreibe vor dem Senden eine Nachricht.",
message_too_long = "Nachrichten dürfen höchstens 2000 Zeichen enthalten.",
message_not_found = "Diese Nachricht ist nicht verfügbar.",
story_limit_reached = "Du hast das Limit aktiver Stories erreicht.",
story_unavailable = "Diese Story ist nicht mehr verfügbar.",
spotlight_unavailable = "Dieser Spotlight ist nicht mehr verfügbar.",
spotlight_limit_reached = "Du hast das Limit aktiver Spotlights erreicht.",
sponsored_limit_reached = "Du hast das Limit aktiver gesponserter Spotlights erreicht.",
ads_disabled = "Gesponserte Spotlights sind deaktiviert.",
invalid_ad_headline = "Gib eine Werbeheadline mit 3 bis 80 Zeichen ein.",
invalid_comment = "Gib einen gültigen Kommentar ein.", comments_disabled = "Kommentare sind deaktiviert.",
comment_not_found = "Dieser Kommentar ist nicht verfügbar.", invalid_report = "Wähle einen gültigen Meldegrund.",
not_authorized = "Du darfst das nicht tun.",
confirmation_required = "Bestätige, dass du deinen SkyPic-Account löschen möchtest.",
too_many_snaps = "Wähle weniger Fotos oder Empfänger.",
rate_limited = "Warte einen Moment und versuche es erneut.",
request_timeout = "Die SkyPic-Anfrage hat zu lange gedauert. Versuche es erneut.",
request_failed = "SkyPic konnte die Anfrage nicht abschließen.",
not_authenticated = "Melde dich zuerst bei Sky Cloud an.",
unknown_error = "SkyPic konnte die Anfrage nicht abschließen.",
default = "SkyPic konnte die Anfrage nicht abschließen.",
},
},
feather = { feather = {
name = "Feather", loading = "Feather wird geladen...", home = "Home", explore = "Erkunden", activity = "Mitteilungen", activityNav = "Mitteilungen", profile = "Profil", back = "Zurück", name = "Feather", loading = "Feather wird geladen...", home = "Home", explore = "Erkunden", activity = "Mitteilungen", activityNav = "Mitteilungen", profile = "Profil", back = "Zurück",
settings = "Einstellungen", settingsEyebrow = "Feather-Einstellungen", compactMode = "Kompakte Timeline", compactModeBody = "Mehr Beiträge auf dem Bildschirm anzeigen.", showSuggestions = "Profilvorschläge", showSuggestionsBody = "Zeige Profile an, denen du vielleicht folgen möchtest.", settings = "Einstellungen", settingsEyebrow = "Feather-Einstellungen", compactMode = "Kompakte Timeline", compactModeBody = "Mehr Beiträge auf dem Bildschirm anzeigen.", showSuggestions = "Profilvorschläge", showSuggestionsBody = "Zeige Profile an, denen du vielleicht folgen möchtest.",
@@ -2049,7 +1880,6 @@ Locales["de"] = {
import_source_unavailable = "Diese Website ist vorübergehend nicht verfügbar.", import_source_unavailable = "Diese Website ist vorübergehend nicht verfügbar.",
invalid_import_url = "Gib einen gültigen HTTPS-Medienlink ein.", import_url_not_allowed = "Dieser Link stammt nicht von der gewählten Website.", invalid_import_url = "Gib einen gültigen HTTPS-Medienlink ein.", import_url_not_allowed = "Dieser Link stammt nicht von der gewählten Website.",
import_url_unavailable = "Die vernetzten Medien konnten nicht erreicht werden.", import_size_unavailable = "Die Website lieferte nicht die Mediengröße.", import_url_unavailable = "Die vernetzten Medien konnten nicht erreicht werden.", import_size_unavailable = "Die Website lieferte nicht die Mediengröße.",
media_in_use = "Dieses Medium wird noch von SkyPic verwendet und kann noch nicht gelöscht werden.",
not_found = "Das Medienobjekt existiert nicht mehr.", profile_photo_required = "Dies ist das letzte Foto in deinem Flare-Profil. Füge vor dem Löschen ein weiteres Profilfoto hinzu.", owner_changed = "Das aktive Telefonkonto hat sich geändert.", not_found = "Das Medienobjekt existiert nicht mehr.", profile_photo_required = "Dies ist das letzte Foto in deinem Flare-Profil. Füge vor dem Löschen ein weiteres Profilfoto hinzu.", owner_changed = "Das aktive Telefonkonto hat sich geändert.",
operation_in_progress = "Eine weitere Medienoperation ist bereits im Gange.", operation_in_progress = "Eine weitere Medienoperation ist bereits im Gange.",
rate_limited = "Zu viele Medienaktionen. Versuch es gleich erneut.", request_failed = "Die Anfrage Fotos ist fehlgeschlagen.", rate_limited = "Zu viele Medienaktionen. Versuch es gleich erneut.", request_failed = "Die Anfrage Fotos ist fehlgeschlagen.",
@@ -2090,7 +1920,7 @@ Locales["de"] = {
health = "Gesundheit, Aktivität und medizinische Informationen", health = "Gesundheit, Aktivität und medizinische Informationen",
["weazel-news"] = "Lokale Nachrichten und Stadtgeschichten", ["weazel-news"] = "Lokale Nachrichten und Stadtgeschichten",
companies = "Unternehmen, Arbeitsplätze und Dienstleistungen", music = "Lieder, Wiedergabelisten und Audio", companies = "Unternehmen, Arbeitsplätze und Dienstleistungen", music = "Lieder, Wiedergabelisten und Audio",
picstagram = "Foto-Sharing und sozialer Feed", skypic = "Private Snaps, Stories und enge Freunde", feather = "Kurze Beiträge und Stadtgespräche", picstagram = "Foto-Sharing und sozialer Feed", feather = "Kurze Beiträge und Stadtgespräche",
fliptok = "Videos und Trends", flare = "Soziale Beiträge und Live-Momente", fliptok = "Videos und Trends", flare = "Soziale Beiträge und Live-Momente",
calendar = "Veranstaltungen, Termine und Erinnerungen", radio = "Live-Radio und Team-Kommunikation", calendar = "Veranstaltungen, Termine und Erinnerungen", radio = "Live-Radio und Team-Kommunikation",
["local-pages"] = "Lokale Unternehmen und Community-Seiten", crewlink = "Crews, Mitglieder und Koordination", ["local-pages"] = "Lokale Unternehmen und Community-Seiten", crewlink = "Crews, Mitglieder und Koordination",
@@ -2116,7 +1946,6 @@ Locales["de"] = {
companies = { first = "Unternehmensverzeichnis", second = "Jobanfragen", third = "Dienstleistungen" }, companies = { first = "Unternehmensverzeichnis", second = "Jobanfragen", third = "Dienstleistungen" },
music = { first = "Aktuelle Wiedergabe", second = "Playlists", third = "Musikmediathek" }, music = { first = "Aktuelle Wiedergabe", second = "Playlists", third = "Musikmediathek" },
picstagram = { first = "Foto-Feed", second = "Stories", third = "Profile" }, picstagram = { first = "Foto-Feed", second = "Stories", third = "Profile" },
skypic = { first = "Private Snaps", second = "Freundes-Stories", third = "Schnelle Chats" },
feather = { first = "Kurzbeiträge", second = "Abonniert-Feed", third = "Unterhaltungen" }, feather = { first = "Kurzbeiträge", second = "Abonniert-Feed", third = "Unterhaltungen" },
fliptok = { first = "Video-Feed", second = "Creator-Werkzeuge", third = "Trends" }, fliptok = { first = "Video-Feed", second = "Creator-Werkzeuge", third = "Trends" },
flare = { first = "Personen entdecken", second = "Matches", third = "Live-Momente" }, flare = { first = "Personen entdecken", second = "Matches", third = "Live-Momente" },
+3 -174
View File
@@ -128,8 +128,8 @@ Locales["en"] = {
}, },
Common = { Common = {
add = "Add", back = "Back", cancel = "Cancel", clear = "Clear", close = "Close", continue = "Continue", delete = "Delete", done = "Done", edit = "Edit", home = "Home", loading = "Loading", pause = "Pause", use = "Use", add = "Add", back = "Back", cancel = "Cancel", clear = "Clear", close = "Close", continue = "Continue", delete = "Delete", done = "Done", edit = "Edit", home = "Home", loading = "Loading", pause = "Pause", use = "Use",
phone = "Phone", phoneStatus = "Phone status", reset = "Reset", retry = "Try Again", phone = "Phone", phoneStatus = "Phone status", reset = "Reset",
loadMore = "Load More", save = "Save", search = "Search", send = "Send", start = "Start", stop = "Stop", save = "Save", search = "Search", send = "Send", start = "Start", stop = "Stop",
signOut = "Sign Out", signingOut = "Signing Out...", signOutTitle = "Sign out of {app}?", signOut = "Sign Out", signingOut = "Signing Out...", signOutTitle = "Sign out of {app}?",
signOutBody = "You will only be signed out of {app}. Your other iFruit apps stay signed in.", signOutFailed = "Could not sign out. Please try again.", signOutBody = "You will only be signed out of {app}. Your other iFruit apps stay signed in.", signOutFailed = "Could not sign out. Please try again.",
appAuth = { appAuth = {
@@ -530,175 +530,6 @@ Locales["en"] = {
default = "Picstagram could not complete the request." default = "Picstagram could not complete the request."
}, },
}, },
skypic = {
name = "SkyPic",
loading = "Loading SkyPic",
navigation = "SkyPic navigation",
time = {
seconds = "{count}s ago", minutes = "{count}m ago",
hours = "{count}h ago", days = "{count}d ago",
},
onboarding = {
title = "Welcome to SkyPic", eyebrow = "Your private camera network",
heading = "Share the moment",
body = "Create a profile for private snaps, close-friend stories, Spotlight videos, and quick chats.",
displayName = "Display name", displayNamePlaceholder = "Your name",
handle = "Handle", handlePlaceholder = "your.handle",
accountBound = "Your SkyPic profile stays linked to this Sky Cloud account.",
create = "Create profile", creating = "Creating...",
},
auth = {
eyebrow = "Your SkyPic account", title = "Welcome back",
body = "Continue with your iFruit account to access your SkyPic profile, snaps, and chats.",
login = "Continue to SkyPic", loggingIn = "Signing in...",
noAccount = "New to SkyPic? Create an iFruit account to get started.",
},
camera = {
eyebrow = "Sky camera", title = "Capture",
body = "Take a photo or video and share it for just a few seconds.",
snap = "Snap", story = "Story", photo = "Photo", video = "Video",
gallery = "Open gallery", capturePhoto = "Take photo", captureVideo = "Record video",
snapHint = "Send it privately to one or more friends.",
storyHint = "Share it with your story for 24 hours.",
},
composer = {
snapTitle = "New snap", storyTitle = "New story", spotlightTitle = "New Spotlight",
send = "Send", addStory = "Add to story", publishSpotlight = "Publish",
caption = "Caption", captionPlaceholder = "Add a caption...",
textOverlay = "Text overlay", textPlaceholder = "Put words on the moment...",
color = "Text color", duration = "View duration", seconds = "{count} seconds",
replay = "Allow one replay",
replayBody = "Recipients may open this snap one additional time.",
recipients = "Recipients", recipientsHint = "Choose one or more friends.", recipientLimit = "Choose up to {count} friends.",
selectedCount = "{count} selected", noFriends = "Add a friend before sending a snap.",
sponsored = "Sponsored post", sponsoredBody = "Clearly label this Spotlight as advertising.",
adHeadline = "Ad headline", adHeadlinePlaceholder = "What should people discover?",
allowComments = "Allow comments", allowCommentsBody = "People can comment on this Spotlight.",
changeMedia = "Change photo or video", sent = "Snap sent.", storyPublished = "Story published.",
spotlightPublished = "Spotlight published.",
},
tabs = { camera = "Camera", chats = "Chats", stories = "Stories", friends = "Friends", spotlight = "Spotlight" },
spotlight = {
title = "Spotlight", add = "Create Spotlight", empty = "No Spotlight videos yet",
emptyBody = "Record the first short video for the SkyPic community.",
videoBy = "Spotlight video by {name}", sponsored = "Sponsored", viewProfile = "View creator profile",
like = "Like", comments = "Comments", views = "Views", noComments = "No comments yet",
commentPlaceholder = "Write a comment...", sendComment = "Send", commentsDisabled = "Comments are disabled for this Spotlight.",
deleteComment = "Delete comment", delete = "Delete Spotlight", deleted = "Spotlight deleted.",
report = "Report Spotlight", reportBody = "Tell us why this Spotlight should be reviewed.",
submitReport = "Submit report", reported = "Spotlight reported.",
navigation = "Spotlight navigation", previous = "Previous Spotlight", next = "Next Spotlight",
reportReasons = {
spam = "Spam or misleading", harassment = "Harassment", dangerous = "Dangerous content",
illegal = "Illegal content", other = "Other",
},
},
chats = {
title = "Chats", incoming = "New snaps", noSnaps = "No unopened snaps",
conversations = "Conversations", noConversations = "Your conversations will appear here.",
start = "Start a conversation", message = "Message", threadPlaceholder = "Write a message...", messageLimit = "Messages can contain up to {count} characters.",
saved = "Saved in chat", save = "Save", unsave = "Unsave", delete = "Delete",
sendSnap = "Send a snap", moreActions = "More actions",
attachPhoto = "Attach photos", takePhoto = "Take photo", emoji = "Emoji", attachVideo = "Attach video",
attachmentPreview = "Selected attachments", removeAttachment = "Remove attachment {number}",
moveAttachmentEarlier = "Move attachment {number} earlier", moveAttachmentLater = "Move attachment {number} later",
attachmentLimit = "You can attach up to {count} photos.",
sendingAttachments = "Sending photos...", photoAttachmentsSent = "Photos sent.",
sending = "Sending...", failed = "Not delivered",
},
snaps = {
newVideo = "New video snap", newPhoto = "New photo snap", replayed = "Replayed",
opened = "Opened", video = "Video snap", photo = "Photo snap", replay = "Replay snap",
},
stories = {
title = "Stories", add = "Add story", yours = "Your story", friends = "Friends",
emptyTitle = "No stories right now",
emptyBody = "Stories from your friends will appear here for 24 hours.",
views = "{count} views", viewers = "Viewers", noViewers = "No views yet",
replyPlaceholder = "Reply to this story...", replySent = "Reply sent.", replyLimit = "Replies can contain up to {count} characters.",
delete = "Delete story", deleted = "Story deleted.", seen = "Seen", unseen = "New",
},
friends = {
title = "Friends", searchPlaceholder = "Search name or @handle",
searchResults = "Search results", score = "{count} points",
requests = "Friend requests", sentRequests = "Sent requests", accept = "Accept", decline = "Decline",
quickAdd = "Quick Add", add = "Add", pending = "Pending", cancelRequest = "Cancel request", all = "Your friends",
empty = "Add friends to start snapping.", remove = "Remove friend", block = "Block", blockedProfiles = "Blocked profiles", unblock = "Unblock",
chat = "Chat", sendSnap = "Send snap", respond = "Respond", friends = "Friends",
requestSent = "Friend request sent to {name}.", requestCanceled = "Friend request to {name} canceled.",
removed = "{name} was removed from your friends.", blocked = "{name} was blocked.", unblocked = "{name} was unblocked.",
},
profile = {
title = "Profile", edit = "Edit profile", save = "Save", cancel = "Cancel",
score = "Snap score", streaks = "Streaks", friends = "Friends", bio = "Bio",
bioPlaceholder = "Tell your friends a little about you...",
storyPrivacy = "Story privacy", privacyFriends = "Friends only", privacyEveryone = "Everyone",
allowStoryReplies = "Allow story replies",
allowStoryRepliesBody = "Friends can reply to your stories in chat.",
showInQuickAdd = "Show in Quick Add",
showInQuickAddBody = "Let other profiles discover you as a suggestion.",
saved = "Profile updated.",
account = "Account", logout = "Sign out", logoutTitle = "Sign out of SkyPic?",
logoutBody = "You will only be signed out of SkyPic. Your profile, snaps, and chats stay available.",
loggingOut = "Signing out...", deleteAccount = "Delete SkyPic account",
deleteAccountTitle = "Delete your SkyPic account?",
deleteAccountBody = "Your SkyPic profile, friends, snaps, stories, Spotlights, comments, and chats will be permanently deleted. Your iFruit account and Photos library stay available.",
deletingAccount = "Deleting account...",
},
viewer = { close = "Close", timeLeft = "{count}s" },
notifications = {
friend_request = "{actor} sent you a friend request.",
friend_accepted = "{actor} accepted your friend request.",
snap = "{actor} sent you a snap.", message = "{actor} sent you a message.",
story_reply = "{actor} replied to your story.", snap_opened = "{actor} opened your snap.",
default = "You have new SkyPic activity.",
},
errors = {
profile_required = "Create your SkyPic profile first.",
profile_exists = "This account already has a SkyPic profile.",
invalid_handle = "Use 3-24 letters, numbers, dots, or underscores.",
handle_taken = "This handle is already taken.",
invalid_display_name = "Enter a display name.", invalid_bio = "Your bio is too long.",
invalid_avatar = "Choose a valid profile photo.", invalid_avatar_seed = "Choose a valid avatar.",
invalid_privacy = "Choose a valid story privacy setting.",
invalid_request = "This friend request is invalid.",
profile_not_found = "This SkyPic profile is unavailable.", blocked = "This profile is blocked.",
friendship_not_found = "This friendship is unavailable.",
friend_request_exists = "A friend request already exists.",
friend_limit_reached = "Your friends list is full.",
request_limit_reached = "Too many open friend requests.",
invalid_recipients = "Choose at least one current friend.",
invalid_media = "Choose media from this phone.",
invalid_media_type = "This media type is not supported.",
invalid_duration = "Choose a view time from 1 to 10 seconds.",
invalid_caption = "This caption is too long.",
invalid_overlay = "This text overlay is too long.",
invalid_color = "Choose a valid overlay color.",
snap_unavailable = "This snap is no longer available.",
replay_unavailable = "This snap cannot be replayed again.",
message_empty = "Write a message before sending.",
message_too_long = "Messages can contain at most 2000 characters.",
message_not_found = "This message is unavailable.",
story_limit_reached = "Your active story limit is reached.",
story_unavailable = "This story is no longer available.",
spotlight_unavailable = "This Spotlight is no longer available.",
spotlight_limit_reached = "Your active Spotlight limit is reached.",
sponsored_limit_reached = "Your active sponsored Spotlight limit is reached.",
ads_disabled = "Sponsored Spotlights are disabled.",
invalid_ad_headline = "Enter an ad headline with 3 to 80 characters.",
invalid_comment = "Enter a valid comment.", comments_disabled = "Comments are disabled.",
comment_not_found = "This comment is unavailable.", invalid_report = "Choose a valid report reason.",
not_authorized = "You are not allowed to do that.",
confirmation_required = "Confirm that you want to delete your SkyPic account.",
too_many_snaps = "Choose fewer photos or recipients.",
rate_limited = "Slow down for a moment and try again.",
request_timeout = "The SkyPic request timed out. Try again.",
request_failed = "SkyPic could not complete the request.",
not_authenticated = "Sign in to Sky Cloud first.",
unknown_error = "SkyPic could not complete the request.",
default = "SkyPic could not complete the request.",
},
},
feather = { feather = {
name = "Feather", loading = "Loading Feather", home = "Home", explore = "Explore", activity = "Notifications", activityNav = "Alerts", profile = "Profile", back = "Back", name = "Feather", loading = "Loading Feather", home = "Home", explore = "Explore", activity = "Notifications", activityNav = "Alerts", profile = "Profile", back = "Back",
settings = "Settings", settingsEyebrow = "Feather preferences", compactMode = "Compact timeline", compactModeBody = "Show more conversation on the screen.", showSuggestions = "Profile suggestions", showSuggestionsBody = "Show people to follow on your profile.", settings = "Settings", settingsEyebrow = "Feather preferences", compactMode = "Compact timeline", compactModeBody = "Show more conversation on the screen.", showSuggestions = "Profile suggestions", showSuggestionsBody = "Show people to follow on your profile.",
@@ -2049,7 +1880,6 @@ Locales["en"] = {
import_source_unavailable = "This website is temporarily unavailable.", import_source_unavailable = "This website is temporarily unavailable.",
invalid_import_url = "Enter a valid HTTPS media link.", import_url_not_allowed = "This link is not from the selected website.", invalid_import_url = "Enter a valid HTTPS media link.", import_url_not_allowed = "This link is not from the selected website.",
import_url_unavailable = "The linked media could not be reached.", import_size_unavailable = "The website did not provide the media size.", import_url_unavailable = "The linked media could not be reached.", import_size_unavailable = "The website did not provide the media size.",
media_in_use = "This media is still used by SkyPic and cannot be deleted yet.",
not_found = "The media item no longer exists.", profile_photo_required = "This is the last photo on your Flare profile. Add another profile photo before deleting it.", owner_changed = "The active phone account changed.", not_found = "The media item no longer exists.", profile_photo_required = "This is the last photo on your Flare profile. Add another profile photo before deleting it.", owner_changed = "The active phone account changed.",
operation_in_progress = "Another media operation is already in progress.", operation_in_progress = "Another media operation is already in progress.",
rate_limited = "Too many media actions. Try again shortly.", request_failed = "The Photos request failed.", rate_limited = "Too many media actions. Try again shortly.", request_failed = "The Photos request failed.",
@@ -2090,7 +1920,7 @@ Locales["en"] = {
health = "Health, activity and medical information", health = "Health, activity and medical information",
["weazel-news"] = "Local news and city stories", ["weazel-news"] = "Local news and city stories",
companies = "Businesses, jobs and services", music = "Songs, playlists and audio", companies = "Businesses, jobs and services", music = "Songs, playlists and audio",
picstagram = "Photo sharing and social feed", skypic = "Private snaps, stories and close friends", feather = "Short posts and city conversations", picstagram = "Photo sharing and social feed", feather = "Short posts and city conversations",
fliptok = "Short videos and trends", flare = "Social posts and live moments", fliptok = "Short videos and trends", flare = "Social posts and live moments",
calendar = "Events, schedules and reminders", radio = "Live radio and team communication", calendar = "Events, schedules and reminders", radio = "Live radio and team communication",
["local-pages"] = "Local businesses and community pages", crewlink = "Crews, members and coordination", ["local-pages"] = "Local businesses and community pages", crewlink = "Crews, members and coordination",
@@ -2116,7 +1946,6 @@ Locales["en"] = {
companies = { first = "Business directory", second = "Job requests", third = "Services" }, companies = { first = "Business directory", second = "Job requests", third = "Services" },
music = { first = "Now playing", second = "Playlists", third = "Music library" }, music = { first = "Now playing", second = "Playlists", third = "Music library" },
picstagram = { first = "Photo feed", second = "Stories", third = "Profiles" }, picstagram = { first = "Photo feed", second = "Stories", third = "Profiles" },
skypic = { first = "Private snaps", second = "Friend stories", third = "Quick chats" },
feather = { first = "Short posts", second = "Following feed", third = "Conversations" }, feather = { first = "Short posts", second = "Following feed", third = "Conversations" },
fliptok = { first = "Video feed", second = "Creator tools", third = "Trends" }, fliptok = { first = "Video feed", second = "Creator tools", third = "Trends" },
flare = { first = "Discover people", second = "Matches", third = "Live moments" }, flare = { first = "Discover people", second = "Matches", third = "Live moments" },
-1
View File
@@ -83,7 +83,6 @@ server_scripts {
'source/server/media_import/fivemanage.lua', 'source/server/media_import/fivemanage.lua',
'source/server/media_import/manifest.lua', 'source/server/media_import/manifest.lua',
'source/server/media.lua', 'source/server/media.lua',
'source/server/skypic.lua',
'source/server/weazel_news.lua', 'source/server/weazel_news.lua',
'source/server/citywarn.lua', 'source/server/citywarn.lua',
'source/server/messages.lua', 'source/server/messages.lua',
-43
View File
@@ -160,37 +160,6 @@ local server_callbacks = {
"picstagram:report", "picstagram:report",
"picstagram:admin-reports", "picstagram:admin-reports",
"picstagram:admin-resolve-report", "picstagram:admin-resolve-report",
"skypic:bootstrap",
"skypic:create-profile",
"skypic:delete-account",
"skypic:update-profile",
"skypic:search",
"skypic:add-friend",
"skypic:respond-friend",
"skypic:remove-friend",
"skypic:block",
"skypic:send-snap",
"skypic:open-snap",
"skypic:replay-snap",
"skypic:publish-story",
"skypic:stories",
"skypic:view-story",
"skypic:story-viewers",
"skypic:remove-story",
"skypic:spotlight-feed",
"skypic:publish-spotlight",
"skypic:view-spotlight",
"skypic:like-spotlight",
"skypic:spotlight-comments",
"skypic:comment-spotlight",
"skypic:delete-spotlight-comment",
"skypic:remove-spotlight",
"skypic:report-spotlight",
"skypic:thread",
"skypic:send-message",
"skypic:mark-thread",
"skypic:save-message",
"skypic:delete-message",
"feather:bootstrap", "feather:bootstrap",
"feather:create-profile", "feather:create-profile",
"feather:update-profile", "feather:update-profile",
@@ -904,18 +873,6 @@ RegisterNetEvent("sky_phone:picstagram:new", function(data)
SendNUIMessage({ type = "picstagram:new", data = data }) SendNUIMessage({ type = "picstagram:new", data = data })
end) end)
RegisterNetEvent("sky_phone:skypic:new", function(data)
local skypic_locale = locale.Nui.Apps.skypic
local notification_text = skypic_locale.notifications[data.kind] or skypic_locale.notifications.default
data.title = skypic_locale.name
data.text = notification_text:gsub("{actor}", tostring(data.actor or ""))
SendNUIMessage({ type = "skypic:new", data = data })
end)
RegisterNetEvent("sky_phone:skypic:changed", function(data)
SendNUIMessage({ type = "skypic:changed", data = data })
end)
RegisterNetEvent("sky_phone:feather:new", function(data) RegisterNetEvent("sky_phone:feather:new", function(data)
local feather_locale = locale.Nui.Apps.feather local feather_locale = locale.Nui.Apps.feather
local notification_text = feather_locale.notifications[data.kind] or feather_locale.notifications.default local notification_text = feather_locale.notifications[data.kind] or feather_locale.notifications.default
-293
View File
@@ -2907,302 +2907,9 @@ local schema = {
}, },
tableOptions = "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci", tableOptions = "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
}, },
{
name = "sky_phone_skypic_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 = "avatar_seed", type = "INT UNSIGNED NOT NULL DEFAULT 1" },
{ name = "story_privacy", type = "ENUM('friends', 'everyone') NOT NULL DEFAULT 'friends'" },
{ name = "quick_add", type = "TINYINT(1) NOT NULL DEFAULT 1" },
{ name = "allow_story_replies", type = "TINYINT(1) NOT NULL DEFAULT 1" },
{ name = "snap_score", type = "BIGINT UNSIGNED NOT NULL DEFAULT 0" },
{ name = "friend_count", type = "INT UNSIGNED 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_skypic_profile_account", columns = "(`account_id`)" },
{ name = "uniq_sky_phone_skypic_profile_handle", columns = "(`handle`)" },
},
indexes = {
{ name = "idx_sky_phone_skypic_quick_add", columns = "(`status`, `quick_add`, `updated_at`)" },
},
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_skypic_friendships",
columns = {
{ name = "id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
{ name = "profile_a_id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
{ name = "profile_b_id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
{ name = "requested_by_id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
{ name = "status", type = "ENUM('pending', 'accepted') NOT NULL DEFAULT 'pending'" },
{ name = "profile_a_last_snap_on", type = "DATE NULL" },
{ name = "profile_b_last_snap_on", type = "DATE NULL" },
{ name = "streak_updated_on", type = "DATE NULL" },
{ name = "streak_count", type = "INT UNSIGNED NOT NULL DEFAULT 0" },
{ name = "best_streak", type = "INT UNSIGNED NOT NULL DEFAULT 0" },
{ name = "accepted_at", type = "DATETIME 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_skypic_friend_pair", columns = "(`profile_a_id`, `profile_b_id`)" },
},
indexes = {
{ name = "idx_sky_phone_skypic_friend_a", columns = "(`profile_a_id`, `status`, `updated_at`)" },
{ name = "idx_sky_phone_skypic_friend_b", columns = "(`profile_b_id`, `status`, `updated_at`)" },
{ name = "idx_sky_phone_skypic_friend_requests", columns = "(`status`, `requested_by_id`, `created_at`)" },
{ name = "idx_sky_phone_skypic_streaks", columns = "(`status`, `streak_updated_on`)" },
},
foreignKeys = {
{ column = "profile_a_id", references = "`sky_phone_skypic_profiles` (`id`) ON DELETE CASCADE" },
{ column = "profile_b_id", references = "`sky_phone_skypic_profiles` (`id`) ON DELETE CASCADE" },
{ column = "requested_by_id", references = "`sky_phone_skypic_profiles` (`id`) ON DELETE CASCADE" },
},
tableOptions = "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
},
{
name = "sky_phone_skypic_blocks",
columns = {
{ name = "blocker_profile_id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
{ name = "blocked_profile_id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
{ name = "created_at", type = "DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP" },
},
primaryKey = { "blocker_profile_id", "blocked_profile_id" },
indexes = {
{ name = "idx_sky_phone_skypic_blocked", columns = "(`blocked_profile_id`, `created_at`)" },
},
foreignKeys = {
{ column = "blocker_profile_id", references = "`sky_phone_skypic_profiles` (`id`) ON DELETE CASCADE" },
{ column = "blocked_profile_id", references = "`sky_phone_skypic_profiles` (`id`) ON DELETE CASCADE" },
},
tableOptions = "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
},
{
name = "sky_phone_skypic_messages",
columns = {
{ name = "id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
{ name = "friendship_id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
{ name = "sender_profile_id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
{ name = "recipient_profile_id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
{ name = "message_type", type = "ENUM('text', 'snap_photo', 'snap_video') NOT NULL" },
{ name = "body", type = "VARCHAR(2000) NOT NULL DEFAULT ''" },
{ name = "caption", type = "VARCHAR(160) NOT NULL DEFAULT ''" },
{ name = "overlay_text", type = "VARCHAR(160) NOT NULL DEFAULT ''" },
{ name = "overlay_color", type = "CHAR(7) NOT NULL DEFAULT '#FFFFFF'", characterSet = "ascii", collation = "ascii_bin" },
{ name = "media_id", type = "BIGINT UNSIGNED NULL" },
{ name = "view_seconds", type = "TINYINT UNSIGNED NULL" },
{ name = "allow_replay", type = "TINYINT(1) NOT NULL DEFAULT 0" },
{ name = "read_at", type = "DATETIME(6) NULL" },
{ name = "opened_at", type = "DATETIME(6) NULL" },
{ name = "replayed_at", type = "DATETIME(6) NULL" },
{ name = "saved_at", type = "DATETIME(6) NULL" },
{ name = "expires_at", type = "DATETIME(6) NULL" },
{ name = "sender_deleted_at", type = "DATETIME(6) NULL" },
{ name = "recipient_deleted_at", type = "DATETIME(6) NULL" },
{ name = "deleted_at", type = "DATETIME(6) NULL" },
{ name = "created_at", type = "DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6)" },
},
primaryKey = "id",
indexes = {
{ name = "idx_sky_phone_skypic_message_thread", columns = "(`friendship_id`, `created_at`, `id`)" },
{ name = "idx_sky_phone_skypic_message_inbox", columns = "(`recipient_profile_id`, `read_at`, `created_at`)" },
{ name = "idx_sky_phone_skypic_message_expiry", columns = "(`message_type`, `expires_at`)" },
{ name = "idx_sky_phone_skypic_message_deleted", columns = "(`deleted_at`)" },
{ name = "idx_sky_phone_skypic_message_media", columns = "(`media_id`)" },
},
foreignKeys = {
{ column = "friendship_id", references = "`sky_phone_skypic_friendships` (`id`) ON DELETE CASCADE" },
{ column = "sender_profile_id", references = "`sky_phone_skypic_profiles` (`id`) ON DELETE CASCADE" },
{ column = "recipient_profile_id", references = "`sky_phone_skypic_profiles` (`id`) ON DELETE CASCADE" },
{ column = "media_id", references = "`sky_phone_media` (`id`) ON DELETE RESTRICT" },
},
tableOptions = "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
},
{
name = "sky_phone_skypic_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 = "caption", type = "VARCHAR(160) NOT NULL DEFAULT ''" },
{ name = "overlay_text", type = "VARCHAR(160) NOT NULL DEFAULT ''" },
{ name = "overlay_color", type = "CHAR(7) NOT NULL DEFAULT '#FFFFFF'", characterSet = "ascii", collation = "ascii_bin" },
{ name = "view_seconds", type = "TINYINT UNSIGNED NOT NULL" },
{ name = "privacy", type = "ENUM('friends', 'everyone') NOT NULL" },
{ name = "status", type = "ENUM('active', 'removed') NOT NULL DEFAULT 'active'" },
{ name = "expires_at", type = "DATETIME(6) NOT NULL" },
{ name = "created_at", type = "DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6)" },
{ name = "updated_at", type = "DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6)" },
},
primaryKey = "id",
indexes = {
{ name = "idx_sky_phone_skypic_story_profile", columns = "(`profile_id`, `status`, `expires_at`)" },
{ name = "idx_sky_phone_skypic_story_expiry", columns = "(`status`, `expires_at`)" },
{ name = "idx_sky_phone_skypic_story_media", columns = "(`media_id`)" },
},
foreignKeys = {
{ column = "profile_id", references = "`sky_phone_skypic_profiles` (`id`) ON DELETE CASCADE" },
{ column = "media_id", references = "`sky_phone_media` (`id`) ON DELETE RESTRICT" },
},
tableOptions = "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
},
{
name = "sky_phone_skypic_story_views",
columns = {
{ name = "story_id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
{ name = "viewer_profile_id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
{ name = "viewed_at", type = "DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6)" },
},
primaryKey = { "story_id", "viewer_profile_id" },
indexes = {
{ name = "idx_sky_phone_skypic_story_viewer", columns = "(`viewer_profile_id`, `viewed_at`)" },
},
foreignKeys = {
{ column = "story_id", references = "`sky_phone_skypic_stories` (`id`) ON DELETE CASCADE" },
{ column = "viewer_profile_id", references = "`sky_phone_skypic_profiles` (`id`) ON DELETE CASCADE" },
},
tableOptions = "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
},
{
name = "sky_phone_skypic_spotlights",
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 = "caption", type = "VARCHAR(160) NOT NULL DEFAULT ''" },
{ name = "overlay_text", type = "VARCHAR(160) NOT NULL DEFAULT ''" },
{ name = "overlay_color", type = "CHAR(7) NOT NULL DEFAULT '#FFFFFF'", characterSet = "ascii", collation = "ascii_bin" },
{ name = "kind", type = "ENUM('organic', 'sponsored') NOT NULL DEFAULT 'organic'" },
{ name = "ad_headline", type = "VARCHAR(80) NOT NULL DEFAULT ''" },
{ name = "comments_enabled", type = "TINYINT(1) NOT NULL DEFAULT 1" },
{ name = "status", type = "ENUM('active', 'removed') NOT NULL DEFAULT 'active'" },
{ name = "expires_at", type = "DATETIME(6) NOT NULL" },
{ name = "created_at", type = "DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6)" },
{ name = "updated_at", type = "DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6)" },
},
primaryKey = "id",
indexes = {
{ name = "idx_sky_phone_skypic_spotlight_feed", columns = "(`status`, `created_at`, `id`)" },
{ name = "idx_sky_phone_skypic_spotlight_profile", columns = "(`profile_id`, `status`, `created_at`)" },
{ name = "idx_sky_phone_skypic_spotlight_expiry", columns = "(`status`, `expires_at`)" },
{ name = "idx_sky_phone_skypic_spotlight_media", columns = "(`media_id`)" },
},
foreignKeys = {
{ column = "profile_id", references = "`sky_phone_skypic_profiles` (`id`) ON DELETE CASCADE" },
{ column = "media_id", references = "`sky_phone_media` (`id`) ON DELETE RESTRICT" },
},
tableOptions = "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
},
{
name = "sky_phone_skypic_spotlight_views",
columns = {
{ name = "spotlight_id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
{ name = "viewer_profile_id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
{ name = "viewed_at", type = "DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6)" },
},
primaryKey = { "spotlight_id", "viewer_profile_id" },
indexes = {
{ name = "idx_sky_phone_skypic_spotlight_viewer", columns = "(`viewer_profile_id`, `viewed_at`)" },
},
foreignKeys = {
{ column = "spotlight_id", references = "`sky_phone_skypic_spotlights` (`id`) ON DELETE CASCADE" },
{ column = "viewer_profile_id", references = "`sky_phone_skypic_profiles` (`id`) ON DELETE CASCADE" },
},
tableOptions = "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
},
{
name = "sky_phone_skypic_spotlight_likes",
columns = {
{ name = "spotlight_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(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6)" },
},
primaryKey = { "spotlight_id", "profile_id" },
indexes = {
{ name = "idx_sky_phone_skypic_spotlight_liker", columns = "(`profile_id`, `created_at`)" },
},
foreignKeys = {
{ column = "spotlight_id", references = "`sky_phone_skypic_spotlights` (`id`) ON DELETE CASCADE" },
{ column = "profile_id", references = "`sky_phone_skypic_profiles` (`id`) ON DELETE CASCADE" },
},
tableOptions = "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
},
{
name = "sky_phone_skypic_spotlight_comments",
columns = {
{ name = "id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
{ name = "spotlight_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 = "body", type = "VARCHAR(500) NOT NULL" },
{ name = "status", type = "ENUM('visible', 'removed') NOT NULL DEFAULT 'visible'" },
{ name = "created_at", type = "DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6)" },
},
primaryKey = "id",
indexes = {
{ name = "idx_sky_phone_skypic_spotlight_comments", columns = "(`spotlight_id`, `status`, `created_at`, `id`)" },
{ name = "idx_sky_phone_skypic_spotlight_comment_author", columns = "(`profile_id`, `created_at`)" },
},
foreignKeys = {
{ column = "spotlight_id", references = "`sky_phone_skypic_spotlights` (`id`) ON DELETE CASCADE" },
{ column = "profile_id", references = "`sky_phone_skypic_profiles` (`id`) ON DELETE CASCADE" },
},
tableOptions = "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
},
{
name = "sky_phone_skypic_spotlight_reports",
columns = {
{ name = "spotlight_id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
{ name = "reporter_profile_id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
{ name = "reason", type = "VARCHAR(32) NOT NULL", characterSet = "ascii", collation = "ascii_general_ci" },
{ name = "details", type = "VARCHAR(500) NOT NULL DEFAULT ''" },
{ name = "status", type = "ENUM('pending', 'reviewed', 'dismissed') NOT NULL DEFAULT 'pending'" },
{ name = "created_at", type = "DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6)" },
},
primaryKey = { "spotlight_id", "reporter_profile_id" },
indexes = {
{ name = "idx_sky_phone_skypic_spotlight_report_queue", columns = "(`status`, `created_at`)" },
},
foreignKeys = {
{ column = "spotlight_id", references = "`sky_phone_skypic_spotlights` (`id`) ON DELETE CASCADE" },
{ column = "reporter_profile_id", references = "`sky_phone_skypic_profiles` (`id`) ON DELETE CASCADE" },
},
tableOptions = "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
},
} }
Bridge.Database.Migrate("sky_phone", schema) Bridge.Database.Migrate("sky_phone", schema)
Bridge.Database.EnsureIndex(
"sky_phone_skypic_profiles",
"uniq_sky_phone_skypic_profile_account",
"(`account_id`)",
{ unique = true }
)
Bridge.Database.EnsureIndex(
"sky_phone_skypic_profiles",
"uniq_sky_phone_skypic_profile_handle",
"(`handle`)",
{ unique = true }
)
Bridge.Database.EnsureIndex(
"sky_phone_skypic_friendships",
"uniq_sky_phone_skypic_friend_pair",
"(`profile_a_id`, `profile_b_id`)",
{ unique = true }
)
Bridge.Database.Query([[ Bridge.Database.Query([[
INSERT IGNORE INTO `sky_phone_fliptok_video_media` (`video_id`, `media_id`, `sort_order`) INSERT IGNORE INTO `sky_phone_fliptok_video_media` (`video_id`, `media_id`, `sort_order`)
SELECT `id`, `media_id`, 1 FROM `sky_phone_fliptok_videos` SELECT `id`, `media_id`, 1 FROM `sky_phone_fliptok_videos`
-1
View File
@@ -37,7 +37,6 @@ local valid_apps = {
phone = true, phone = true,
photos = true, photos = true,
picstagram = true, picstagram = true,
skypic = true,
} }
local valid_visibilities = { contacts = true, everyone = true, hidden = true } local valid_visibilities = { contacts = true, everyone = true, hidden = true }
+7 -84
View File
@@ -20,16 +20,6 @@ local allowed_remote_mimes = {
}, },
} }
local function affected_rows(result)
if type(result) == "number" then
return result
end
if type(result) == "table" then
return tonumber(result.affectedRows) or tonumber(result.affected_rows) or 0
end
return 0
end
local function media_config() local function media_config()
return Config.Media.FiveManage return Config.Media.FiveManage
end end
@@ -831,24 +821,6 @@ local function is_required_flare_profile_photo(media_id)
return rows[1] ~= nil return rows[1] ~= nil
end end
local function is_referenced_by_skypic(media_id)
local rows = Bridge.Database.Query([[
SELECT 1 AS `in_use`
FROM `sky_phone_skypic_messages`
WHERE `media_id` = ?
UNION ALL
SELECT 1 AS `in_use`
FROM `sky_phone_skypic_stories`
WHERE `media_id` = ?
UNION ALL
SELECT 1 AS `in_use`
FROM `sky_phone_skypic_spotlights`
WHERE `media_id` = ?
LIMIT 1
]], { media_id, media_id, media_id })
return rows[1] ~= nil
end
local function delete_owned_media(src, owner, media_id) local function delete_owned_media(src, owner, media_id)
local condition, params = owner_condition(owner) local condition, params = owner_condition(owner)
local query_params = { media_id } local query_params = { media_id }
@@ -866,74 +838,25 @@ local function delete_owned_media(src, owner, media_id)
if row.media_type == "photo" and is_required_flare_profile_photo(media_id) then if row.media_type == "photo" and is_required_flare_profile_photo(media_id) then
return false, "profile_photo_required" return false, "profile_photo_required"
end end
-- All SkyPic media foreign keys are RESTRICT. Keep the remote object intact until
-- cleanup has physically removed every referencing row.
if is_referenced_by_skypic(media_id) then
return false, "media_in_use"
end
local delete_key = row.origin == "phone_upload" and row.remote_id or ("import:%s"):format(media_id) local delete_key = row.origin == "phone_upload" and row.remote_id or ("import:%s"):format(media_id)
if pending_deletes[delete_key] then if pending_deletes[delete_key] then
return false, "operation_in_progress" return false, "operation_in_progress"
end end
pending_deletes[delete_key] = src pending_deletes[delete_key] = src
local delete_remote = false
if row.origin == "phone_upload" then if row.origin == "phone_upload" then
local references = Bridge.Database.Query( local references = Bridge.Database.Query(
"SELECT COUNT(*) AS `count` FROM `sky_phone_media` WHERE `remote_id` = ?", "SELECT COUNT(*) AS `count` FROM `sky_phone_media` WHERE `remote_id` = ?",
{ row.remote_id } { row.remote_id }
) )
delete_remote = (tonumber(references[1] and references[1].count) or 0) <= 1 if (tonumber(references[1] and references[1].count) or 0) <= 1 then
end local deleted, delete_error = delete_remote_file(row.remote_id)
-- Delete the database parent first and repeat the SkyPic guard inside the if not deleted then
-- same statement. The RESTRICT foreign keys then make this atomic against pending_deletes[delete_key] = nil
-- a concurrent snap/story/spotlight insert: either that insert wins and this DELETE return false, delete_error
-- affects zero rows, or this DELETE wins and the later insert cannot refer end
-- to a missing media row.
local delete_params = {}
for _, value in ipairs(query_params) do
delete_params[#delete_params + 1] = value
end
delete_params[#delete_params + 1] = media_id
delete_params[#delete_params + 1] = media_id
delete_params[#delete_params + 1] = media_id
local result = Bridge.Database.Query(([[
DELETE FROM `sky_phone_media`
WHERE `id` = ? AND %s
AND NOT EXISTS (
SELECT 1 FROM `sky_phone_skypic_messages`
WHERE `media_id` = ?
)
AND NOT EXISTS (
SELECT 1 FROM `sky_phone_skypic_stories`
WHERE `media_id` = ?
)
AND NOT EXISTS (
SELECT 1 FROM `sky_phone_skypic_spotlights`
WHERE `media_id` = ?
)
]]):format(condition), delete_params)
if affected_rows(result) ~= 1 then
pending_deletes[delete_key] = nil
if is_referenced_by_skypic(media_id) then
return false, "media_in_use"
end
return false, "not_found"
end
if delete_remote then
local deleted, delete_error = delete_remote_file(row.remote_id)
if not deleted then
-- The user-visible record is already gone and no new FK reference
-- can be created. Report the logical delete as complete and leave a
-- precise audit trail for provider-side orphan cleanup.
Bridge.Debug(
"error",
"[sky_phone] Media %s was deleted locally but remote object %s could not be removed (%s).",
tostring(media_id),
tostring(row.remote_id),
tostring(delete_error)
)
end end
end end
Bridge.Database.Query(("DELETE FROM `sky_phone_media` WHERE `id` = ? AND %s"):format(condition), query_params)
pending_deletes[delete_key] = nil pending_deletes[delete_key] = nil
return true return true
end end
+2 -30
View File
@@ -910,16 +910,12 @@ function SkyPhone.NotifyAccount(account_id, event_name, data)
end end
end end
function SkyPhone.NotifyAccountDevices(account_id, event_name, data, required_app_auth) function SkyPhone.NotifyAccountDevices(account_id, event_name, data)
local rows = Bridge.Database.Query([[ local rows = Bridge.Database.Query([[
SELECT d.`imei`, d.`device_name`, account.`email` AS `account_email`, SELECT d.`imei`, d.`device_name`, settings.`payload` AS `settings`
settings.`payload` AS `settings`, app_auth.`payload` AS `app_auth`
FROM `sky_phone_devices` d FROM `sky_phone_devices` d
JOIN `sky_phone_accounts` account ON account.`id` = d.`account_id`
LEFT JOIN `sky_phone_device_data` settings LEFT JOIN `sky_phone_device_data` settings
ON settings.`device_imei` = d.`imei` AND settings.`namespace` = 'settings' ON settings.`device_imei` = d.`imei` AND settings.`namespace` = 'settings'
LEFT JOIN `sky_phone_device_data` app_auth
ON app_auth.`device_imei` = d.`imei` AND app_auth.`namespace` = 'appAuth'
WHERE d.`account_id` = ? WHERE d.`account_id` = ?
]], { account_id }) ]], { account_id })
local devices = {} local devices = {}
@@ -927,31 +923,7 @@ function SkyPhone.NotifyAccountDevices(account_id, event_name, data, required_ap
devices[row.imei] = row devices[row.imei] = row
end end
local function has_required_app_session(device)
if required_app_auth == nil then
return true
end
if type(required_app_auth) ~= 'string' or type(device.app_auth) ~= 'string' then
return false
end
local decoded, app_auth = pcall(json.decode, device.app_auth)
if not decoded or type(app_auth) ~= 'table' or app_auth.version ~= 1
or app_auth.accountEmail ~= device.account_email or type(app_auth.signedIn) ~= 'table'
then
return false
end
for _, app_id in ipairs(app_auth.signedIn) do
if app_id == required_app_auth then
return true
end
end
return false
end
local function notify_device(source, device) local function notify_device(source, device)
if not has_required_app_session(device) then
return
end
local payload = {} local payload = {}
for key, value in pairs(data) do for key, value in pairs(data) do
payload[key] = value payload[key] = value
File diff suppressed because it is too large Load Diff
-1
View File
@@ -66,7 +66,6 @@ local RESERVED_APP_IDS = {
phone = true, phone = true,
photos = true, photos = true,
picstagram = true, picstagram = true,
skypic = true,
radio = true, radio = true,
settings = true, settings = true,
["sky-flappy"] = true, ["sky-flappy"] = true,
-194
View File
@@ -1483,197 +1483,3 @@ CREATE TABLE IF NOT EXISTS `sky_phone_crypto_audit_events` (
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
KEY `idx_sky_phone_crypto_audit` (`profile_id`,`created_at`,`id`) KEY `idx_sky_phone_crypto_audit` (`profile_id`,`created_at`,`id`)
) ENGINE=InnoDB; ) ENGINE=InnoDB;
CREATE TABLE IF NOT EXISTS `sky_phone_skypic_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,
`avatar_seed` INT UNSIGNED NOT NULL DEFAULT 1,
`story_privacy` ENUM('friends','everyone') NOT NULL DEFAULT 'friends',
`quick_add` TINYINT(1) NOT NULL DEFAULT 1,
`allow_story_replies` TINYINT(1) NOT NULL DEFAULT 1,
`snap_score` BIGINT UNSIGNED NOT NULL DEFAULT 0,
`friend_count` INT UNSIGNED 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_skypic_profile_account` (`account_id`),
UNIQUE KEY `uniq_sky_phone_skypic_profile_handle` (`handle`),
KEY `idx_sky_phone_skypic_quick_add` (`status`,`quick_add`,`updated_at`),
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_skypic_friendships` (
`id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`profile_a_id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`profile_b_id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`requested_by_id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`status` ENUM('pending','accepted') NOT NULL DEFAULT 'pending',
`profile_a_last_snap_on` DATE NULL,
`profile_b_last_snap_on` DATE NULL,
`streak_updated_on` DATE NULL,
`streak_count` INT UNSIGNED NOT NULL DEFAULT 0,
`best_streak` INT UNSIGNED NOT NULL DEFAULT 0,
`accepted_at` DATETIME 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_skypic_friend_pair` (`profile_a_id`,`profile_b_id`),
KEY `idx_sky_phone_skypic_friend_a` (`profile_a_id`,`status`,`updated_at`),
KEY `idx_sky_phone_skypic_friend_b` (`profile_b_id`,`status`,`updated_at`),
KEY `idx_sky_phone_skypic_friend_requests` (`status`,`requested_by_id`,`created_at`),
KEY `idx_sky_phone_skypic_streaks` (`status`,`streak_updated_on`),
FOREIGN KEY (`profile_a_id`) REFERENCES `sky_phone_skypic_profiles` (`id`) ON DELETE CASCADE,
FOREIGN KEY (`profile_b_id`) REFERENCES `sky_phone_skypic_profiles` (`id`) ON DELETE CASCADE,
FOREIGN KEY (`requested_by_id`) REFERENCES `sky_phone_skypic_profiles` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `sky_phone_skypic_blocks` (
`blocker_profile_id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`blocked_profile_id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`blocker_profile_id`,`blocked_profile_id`),
KEY `idx_sky_phone_skypic_blocked` (`blocked_profile_id`,`created_at`),
FOREIGN KEY (`blocker_profile_id`) REFERENCES `sky_phone_skypic_profiles` (`id`) ON DELETE CASCADE,
FOREIGN KEY (`blocked_profile_id`) REFERENCES `sky_phone_skypic_profiles` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `sky_phone_skypic_messages` (
`id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`friendship_id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`sender_profile_id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`recipient_profile_id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`message_type` ENUM('text','snap_photo','snap_video') NOT NULL,
`body` VARCHAR(2000) NOT NULL DEFAULT '',
`caption` VARCHAR(160) NOT NULL DEFAULT '',
`overlay_text` VARCHAR(160) NOT NULL DEFAULT '',
`overlay_color` CHAR(7) CHARACTER SET ascii COLLATE ascii_bin NOT NULL DEFAULT '#FFFFFF',
`media_id` BIGINT UNSIGNED NULL,
`view_seconds` TINYINT UNSIGNED NULL,
`allow_replay` TINYINT(1) NOT NULL DEFAULT 0,
`read_at` DATETIME(6) NULL,
`opened_at` DATETIME(6) NULL,
`replayed_at` DATETIME(6) NULL,
`saved_at` DATETIME(6) NULL,
`expires_at` DATETIME(6) NULL,
`sender_deleted_at` DATETIME(6) NULL,
`recipient_deleted_at` DATETIME(6) NULL,
`deleted_at` DATETIME(6) NULL,
`created_at` DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
PRIMARY KEY (`id`),
KEY `idx_sky_phone_skypic_message_thread` (`friendship_id`,`created_at`,`id`),
KEY `idx_sky_phone_skypic_message_inbox` (`recipient_profile_id`,`read_at`,`created_at`),
KEY `idx_sky_phone_skypic_message_expiry` (`message_type`,`expires_at`),
KEY `idx_sky_phone_skypic_message_deleted` (`deleted_at`),
KEY `idx_sky_phone_skypic_message_media` (`media_id`),
FOREIGN KEY (`friendship_id`) REFERENCES `sky_phone_skypic_friendships` (`id`) ON DELETE CASCADE,
FOREIGN KEY (`sender_profile_id`) REFERENCES `sky_phone_skypic_profiles` (`id`) ON DELETE CASCADE,
FOREIGN KEY (`recipient_profile_id`) REFERENCES `sky_phone_skypic_profiles` (`id`) ON DELETE CASCADE,
FOREIGN KEY (`media_id`) REFERENCES `sky_phone_media` (`id`) ON DELETE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `sky_phone_skypic_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,
`caption` VARCHAR(160) NOT NULL DEFAULT '',
`overlay_text` VARCHAR(160) NOT NULL DEFAULT '',
`overlay_color` CHAR(7) CHARACTER SET ascii COLLATE ascii_bin NOT NULL DEFAULT '#FFFFFF',
`view_seconds` TINYINT UNSIGNED NOT NULL,
`privacy` ENUM('friends','everyone') NOT NULL,
`status` ENUM('active','removed') NOT NULL DEFAULT 'active',
`expires_at` DATETIME(6) NOT NULL,
`created_at` DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
`updated_at` DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
PRIMARY KEY (`id`),
KEY `idx_sky_phone_skypic_story_profile` (`profile_id`,`status`,`expires_at`),
KEY `idx_sky_phone_skypic_story_expiry` (`status`,`expires_at`),
KEY `idx_sky_phone_skypic_story_media` (`media_id`),
FOREIGN KEY (`profile_id`) REFERENCES `sky_phone_skypic_profiles` (`id`) ON DELETE CASCADE,
FOREIGN KEY (`media_id`) REFERENCES `sky_phone_media` (`id`) ON DELETE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `sky_phone_skypic_story_views` (
`story_id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`viewer_profile_id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`viewed_at` DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
PRIMARY KEY (`story_id`,`viewer_profile_id`),
KEY `idx_sky_phone_skypic_story_viewer` (`viewer_profile_id`,`viewed_at`),
FOREIGN KEY (`story_id`) REFERENCES `sky_phone_skypic_stories` (`id`) ON DELETE CASCADE,
FOREIGN KEY (`viewer_profile_id`) REFERENCES `sky_phone_skypic_profiles` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `sky_phone_skypic_spotlights` (
`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,
`caption` VARCHAR(160) NOT NULL DEFAULT '',
`overlay_text` VARCHAR(160) NOT NULL DEFAULT '',
`overlay_color` CHAR(7) CHARACTER SET ascii COLLATE ascii_bin NOT NULL DEFAULT '#FFFFFF',
`kind` ENUM('organic','sponsored') NOT NULL DEFAULT 'organic',
`ad_headline` VARCHAR(80) NOT NULL DEFAULT '',
`comments_enabled` TINYINT(1) NOT NULL DEFAULT 1,
`status` ENUM('active','removed') NOT NULL DEFAULT 'active',
`expires_at` DATETIME(6) NOT NULL,
`created_at` DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
`updated_at` DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
PRIMARY KEY (`id`),
KEY `idx_sky_phone_skypic_spotlight_feed` (`status`,`created_at`,`id`),
KEY `idx_sky_phone_skypic_spotlight_profile` (`profile_id`,`status`,`created_at`),
KEY `idx_sky_phone_skypic_spotlight_expiry` (`status`,`expires_at`),
KEY `idx_sky_phone_skypic_spotlight_media` (`media_id`),
FOREIGN KEY (`profile_id`) REFERENCES `sky_phone_skypic_profiles` (`id`) ON DELETE CASCADE,
FOREIGN KEY (`media_id`) REFERENCES `sky_phone_media` (`id`) ON DELETE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `sky_phone_skypic_spotlight_views` (
`spotlight_id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`viewer_profile_id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`viewed_at` DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
PRIMARY KEY (`spotlight_id`,`viewer_profile_id`),
KEY `idx_sky_phone_skypic_spotlight_viewer` (`viewer_profile_id`,`viewed_at`),
FOREIGN KEY (`spotlight_id`) REFERENCES `sky_phone_skypic_spotlights` (`id`) ON DELETE CASCADE,
FOREIGN KEY (`viewer_profile_id`) REFERENCES `sky_phone_skypic_profiles` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `sky_phone_skypic_spotlight_likes` (
`spotlight_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(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
PRIMARY KEY (`spotlight_id`,`profile_id`),
KEY `idx_sky_phone_skypic_spotlight_liker` (`profile_id`,`created_at`),
FOREIGN KEY (`spotlight_id`) REFERENCES `sky_phone_skypic_spotlights` (`id`) ON DELETE CASCADE,
FOREIGN KEY (`profile_id`) REFERENCES `sky_phone_skypic_profiles` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `sky_phone_skypic_spotlight_comments` (
`id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`spotlight_id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`profile_id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`body` VARCHAR(500) NOT NULL,
`status` ENUM('visible','removed') NOT NULL DEFAULT 'visible',
`created_at` DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
PRIMARY KEY (`id`),
KEY `idx_sky_phone_skypic_spotlight_comments` (`spotlight_id`,`status`,`created_at`,`id`),
KEY `idx_sky_phone_skypic_spotlight_comment_author` (`profile_id`,`created_at`),
FOREIGN KEY (`spotlight_id`) REFERENCES `sky_phone_skypic_spotlights` (`id`) ON DELETE CASCADE,
FOREIGN KEY (`profile_id`) REFERENCES `sky_phone_skypic_profiles` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `sky_phone_skypic_spotlight_reports` (
`spotlight_id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`reporter_profile_id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`reason` VARCHAR(32) CHARACTER SET ascii COLLATE ascii_general_ci NOT NULL,
`details` VARCHAR(500) NOT NULL DEFAULT '',
`status` ENUM('pending','reviewed','dismissed') NOT NULL DEFAULT 'pending',
`created_at` DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
PRIMARY KEY (`spotlight_id`,`reporter_profile_id`),
KEY `idx_sky_phone_skypic_spotlight_report_queue` (`status`,`created_at`),
FOREIGN KEY (`spotlight_id`) REFERENCES `sky_phone_skypic_spotlights` (`id`) ON DELETE CASCADE,
FOREIGN KEY (`reporter_profile_id`) REFERENCES `sky_phone_skypic_profiles` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;