From 54a358ddd0fed065895f8ed99935f54cb8174b63 Mon Sep 17 00:00:00 2001 From: Dominik Date: Fri, 21 Aug 2026 23:01:06 +0200 Subject: [PATCH] feat(skypic): add spotlight and polish social flows --- .../src/assets/img/app-icons/skypic-v2.jpg | Bin 0 -> 131400 bytes frontend/src/config/apps.ts | 2 +- frontend/src/skypic.backend.contract.test.ts | 45 +- .../src/skypic.integration.contract.test.ts | 9 + frontend/src/stores/phone.ts | 57 +- frontend/src/stores/skypic.ts | 266 ++++ frontend/src/types/skypic.ts | 48 +- .../src/views/apps/SkyPicApp.contract.test.ts | 73 +- frontend/src/views/apps/SkyPicApp.vue | 1182 +++++++++++++++-- frontend/testserver/index.cjs | 360 +++++ frontend/testserver/smoke.cjs | 103 ++ sky_phone/config/config.lua | 15 + sky_phone/config/locales/de.lua | 35 +- sky_phone/config/locales/en.lua | 35 +- sky_phone/source/client/main.lua | 9 + sky_phone/source/server/db_migrate.lua | 105 ++ sky_phone/source/server/media.lua | 15 +- sky_phone/source/server/skypic.lua | 490 +++++++ sky_phone/sql/install.sql | 70 + 19 files changed, 2765 insertions(+), 154 deletions(-) create mode 100644 frontend/src/assets/img/app-icons/skypic-v2.jpg diff --git a/frontend/src/assets/img/app-icons/skypic-v2.jpg b/frontend/src/assets/img/app-icons/skypic-v2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0f4ff5e0bdb18402d1d911d5ef38e8aba75d7805 GIT binary patch literal 131400 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�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

ad)50zO_lt?{(}G8o1C*=;<|(- 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 U_>`@DoYMb4v;$Bs_ILFE0W; = { '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', () => { @@ -150,7 +192,7 @@ describe('SkyPic backend contracts', () => { } } - for (const table of ['messages', 'stories']) { + for (const table of ['messages', 'stories', 'spotlights']) { expect(migrationTable(table)).toContain('ON DELETE RESTRICT') expect(installTable(table)).toContain('ON DELETE RESTRICT') } @@ -445,6 +487,7 @@ describe('SkyPic backend contracts', () => { ) 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'") diff --git a/frontend/src/skypic.integration.contract.test.ts b/frontend/src/skypic.integration.contract.test.ts index cc7c97f..cb8b69b 100644 --- a/frontend/src/skypic.integration.contract.test.ts +++ b/frontend/src/skypic.integration.contract.test.ts @@ -35,6 +35,15 @@ const callbacks = [ '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', diff --git a/frontend/src/stores/phone.ts b/frontend/src/stores/phone.ts index fb99a62..153938c 100644 --- a/frontend/src/stores/phone.ts +++ b/frontend/src/stores/phone.ts @@ -1657,7 +1657,7 @@ const defaultLocales: LocaleTree = { title: 'Welcome to SkyPic', eyebrow: 'Your private camera network', heading: 'Share the moment', - body: 'Create a profile for private snaps, close-friend stories, and quick chats.', + body: 'Create a profile for private snaps, close-friend stories, Spotlight videos, and quick chats.', displayName: 'Display name', displayNamePlaceholder: 'Your name', handle: 'Handle', @@ -1692,8 +1692,10 @@ const defaultLocales: LocaleTree = { 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', @@ -1708,15 +1710,56 @@ const defaultLocales: LocaleTree = { 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', @@ -1831,7 +1874,7 @@ const defaultLocales: LocaleTree = { deleteAccount: 'Delete SkyPic account', deleteAccountTitle: 'Delete your SkyPic account?', deleteAccountBody: - 'Your SkyPic profile, friends, snaps, stories, and chats will be permanently deleted. Your iFruit account and Photos library stay available.', + '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: { @@ -1878,6 +1921,16 @@ const defaultLocales: LocaleTree = { 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.', diff --git a/frontend/src/stores/skypic.ts b/frontend/src/stores/skypic.ts index d1bf5f0..48c8330 100644 --- a/frontend/src/stores/skypic.ts +++ b/frontend/src/stores/skypic.ts @@ -12,9 +12,13 @@ import type { SkyPicOpenedSnap, SkyPicProfile, SkyPicProfileSummary, + SkyPicPublishSpotlightInput, SkyPicPublishStoryInput, SkyPicSendSnapInput, SkyPicSnap, + SkyPicSpotlight, + SkyPicSpotlightComment, + SkyPicSpotlightReportReason, SkyPicStory, SkyPicStoryViewer, SkyPicThread, @@ -27,6 +31,8 @@ const MAX_MESSAGE_CHARACTERS = 2_000 const MAX_SNAP_MEDIA = 10 const MAX_TEXT_OVERLAY_CHARACTERS = 160 const STORY_PAGE_SIZE = 30 +const SPOTLIGHT_PAGE_SIZE = 12 +const SPOTLIGHT_COMMENT_PAGE_SIZE = 50 export function normalizeSkyPicHandle(value: string): string { return value.trim().toLowerCase() @@ -75,6 +81,8 @@ export const useSkyPicStore = defineStore('skypic', () => { const conversations = ref([]) const inbox = ref([]) const stories = ref([]) + const spotlights = ref([]) + const spotlightComments = ref([]) const suggestions = ref([]) const searchResults = ref([]) const unreadCount = ref(0) @@ -87,6 +95,8 @@ export const useSkyPicStore = defineStore('skypic', () => { const storyViewers = ref([]) const storiesHasMore = ref(true) const storyViewersHasMore = ref(true) + const spotlightsHasMore = ref(true) + const spotlightCommentsHasMore = ref(true) const loading = ref(false) const bootstrapPending = ref(false) @@ -98,6 +108,9 @@ export const useSkyPicStore = defineStore('skypic', () => { const storyViewing = ref(false) const storiesLoadingMore = ref(false) const storyViewersLoadingMore = ref(false) + const spotlightsLoading = ref(false) + const spotlightsLoadingMore = ref(false) + const spotlightCommentsLoading = ref(false) const error = ref(null) let bootstrapRequest = 0 let bootstrapGeneration = 0 @@ -109,6 +122,8 @@ export const useSkyPicStore = defineStore('skypic', () => { let sessionVersion = 0 let hydrationRevision = 0 let storyViewersStoryId: string | null = null + let spotlightCommentsSpotlightId: string | null = null + const viewedSpotlightIds = new Set() let bootstrapInFlight: { promise: Promise session: number @@ -207,6 +222,8 @@ export const useSkyPicStore = defineStore('skypic', () => { storyViewersRequest += 1 threadRequest += 1 storyViewersStoryId = null + spotlightCommentsSpotlightId = null + viewedSpotlightIds.clear() profile.value = null blockedProfiles.value = [] friends.value = [] @@ -214,6 +231,8 @@ export const useSkyPicStore = defineStore('skypic', () => { conversations.value = [] inbox.value = [] stories.value = [] + spotlights.value = [] + spotlightComments.value = [] suggestions.value = [] searchResults.value = [] unreadCount.value = 0 @@ -225,6 +244,8 @@ export const useSkyPicStore = defineStore('skypic', () => { storyViewers.value = [] storiesHasMore.value = true storyViewersHasMore.value = true + spotlightsHasMore.value = true + spotlightCommentsHasMore.value = true loading.value = false threadLoading.value = false searchLoading.value = false @@ -232,6 +253,9 @@ export const useSkyPicStore = defineStore('skypic', () => { storyViewing.value = false storiesLoadingMore.value = false storyViewersLoadingMore.value = false + spotlightsLoading.value = false + spotlightsLoadingMore.value = false + spotlightCommentsLoading.value = false error.value = null } @@ -799,6 +823,230 @@ export const useSkyPicStore = defineStore('skypic', () => { return response } + async function loadSpotlights(): Promise { + spotlightsLoading.value = true + const response = await sessionCall( + 'skypic:spotlight-feed', + { offset: 0 }, + ) + spotlightsLoading.value = false + setError(response) + if (!response.success || !response.data) return false + spotlights.value = response.data + spotlightsHasMore.value = response.data.length >= SPOTLIGHT_PAGE_SIZE + return true + } + + async function loadMoreSpotlights(): Promise { + if (!spotlightsHasMore.value || spotlightsLoadingMore.value) return true + spotlightsLoadingMore.value = true + const response = await sessionCall( + 'skypic:spotlight-feed', + { offset: spotlights.value.length }, + ) + spotlightsLoadingMore.value = false + setError(response) + if (!response.success || !response.data) return false + spotlights.value = uniqueById([...spotlights.value, ...response.data]) + spotlightsHasMore.value = response.data.length >= SPOTLIGHT_PAGE_SIZE + return true + } + + async function publishSpotlight( + input: SkyPicPublishSpotlightInput, + ): Promise> { + const payload: SkyPicPublishSpotlightInput = { + ...input, + adHeadline: Array.from(input.adHeadline.trim()).slice(0, 80).join(''), + caption: input.caption.trim(), + durationSeconds: clampDuration(input.durationSeconds), + mediaType: 'video', + overlayColor: normalizeColor(input.overlayColor), + textOverlay: Array.from(input.textOverlay.trim()) + .slice(0, MAX_TEXT_OVERLAY_CHARACTERS) + .join(''), + } + const response = await sessionCall( + 'skypic:publish-spotlight', + payload, + ) + setError(response) + if (response.success && response.data) { + spotlights.value = uniqueById([response.data, ...spotlights.value]) + } + return response + } + + async function viewSpotlight(spotlightId: string): Promise { + if (viewedSpotlightIds.has(spotlightId)) return true + viewedSpotlightIds.add(spotlightId) + const response = await sessionCall<{ viewCount: number }>( + 'skypic:view-spotlight', + { spotlightId }, + ) + setError(response) + if (!response.success || !response.data) { + viewedSpotlightIds.delete(spotlightId) + return false + } + spotlights.value = spotlights.value.map((spotlight) => + spotlight.id === spotlightId + ? { + ...spotlight, + isViewed: true, + viewCount: Math.max( + 0, + Number(response.data?.viewCount) || spotlight.viewCount, + ), + } + : spotlight, + ) + return true + } + + async function likeSpotlight( + spotlightId: string, + active: boolean, + ): Promise { + const response = await sessionCall<{ active: boolean; likeCount: number }>( + 'skypic:like-spotlight', + { active, spotlightId }, + ) + setError(response) + if (!response.success || !response.data) return false + spotlights.value = spotlights.value.map((spotlight) => + spotlight.id === spotlightId + ? { + ...spotlight, + isLiked: response.data?.active ?? active, + likeCount: Math.max(0, Number(response.data?.likeCount) || 0), + } + : spotlight, + ) + return true + } + + async function loadSpotlightComments(spotlightId: string): Promise { + spotlightCommentsLoading.value = true + const response = await sessionCall( + 'skypic:spotlight-comments', + { offset: 0, spotlightId }, + ) + spotlightCommentsLoading.value = false + setError(response) + if (!response.success || !response.data) return false + spotlightCommentsSpotlightId = spotlightId + spotlightComments.value = response.data + spotlightCommentsHasMore.value = + response.data.length >= SPOTLIGHT_COMMENT_PAGE_SIZE + return true + } + + async function loadMoreSpotlightComments( + spotlightId: string, + ): Promise { + if ( + spotlightCommentsSpotlightId !== spotlightId || + !spotlightCommentsHasMore.value || + spotlightCommentsLoading.value + ) { + return true + } + spotlightCommentsLoading.value = true + const response = await sessionCall( + 'skypic:spotlight-comments', + { offset: spotlightComments.value.length, spotlightId }, + ) + spotlightCommentsLoading.value = false + setError(response) + if (!response.success || !response.data) return false + spotlightComments.value = uniqueById([ + ...spotlightComments.value, + ...response.data, + ]) + spotlightCommentsHasMore.value = + response.data.length >= SPOTLIGHT_COMMENT_PAGE_SIZE + return true + } + + async function commentSpotlight( + spotlightId: string, + body: string, + ): Promise { + const response = await sessionCall( + 'skypic:comment-spotlight', + { body: Array.from(body.trim()).slice(0, 500).join(''), spotlightId }, + ) + setError(response) + if (!response.success || !response.data) return false + spotlightCommentsSpotlightId = spotlightId + spotlightComments.value = uniqueById([ + response.data, + ...spotlightComments.value, + ]) + spotlights.value = spotlights.value.map((spotlight) => + spotlight.id === spotlightId + ? { ...spotlight, commentCount: spotlight.commentCount + 1 } + : spotlight, + ) + return true + } + + async function deleteSpotlightComment(commentId: string): Promise { + const comment = spotlightComments.value.find( + (item) => item.id === commentId, + ) + const response = await sessionCall('skypic:delete-spotlight-comment', { + commentId, + }) + setError(response) + if (!response.success) return false + spotlightComments.value = spotlightComments.value.filter( + (item) => item.id !== commentId, + ) + if (comment) { + spotlights.value = spotlights.value.map((spotlight) => + spotlight.id === comment.spotlightId + ? { + ...spotlight, + commentCount: Math.max(0, spotlight.commentCount - 1), + } + : spotlight, + ) + } + return true + } + + async function removeSpotlight(spotlightId: string): Promise { + const response = await sessionCall('skypic:remove-spotlight', { + spotlightId, + }) + setError(response) + if (!response.success) return false + spotlights.value = spotlights.value.filter( + (spotlight) => spotlight.id !== spotlightId, + ) + return true + } + + async function reportSpotlight( + spotlightId: string, + reason: SkyPicSpotlightReportReason, + details = '', + ): Promise { + const response = await sessionCall('skypic:report-spotlight', { + details: Array.from(details.trim()).slice(0, 500).join(''), + reason, + spotlightId, + }) + setError(response) + if (!response.success) return false + spotlights.value = spotlights.value.filter( + (spotlight) => spotlight.id !== spotlightId, + ) + return true + } + async function viewStory( storyId: string, ): Promise> { @@ -1104,15 +1352,21 @@ export const useSkyPicStore = defineStore('skypic', () => { clearViewedStory, closeThread, conversations, + commentSpotlight, createProfile, deleteAccount, deleteMessage, + deleteSpotlightComment, error, friends, hasProfile, inbox, incomingRequests, loadStories, + loadSpotlights, + loadMoreSpotlights, + loadSpotlightComments, + loadMoreSpotlightComments, loadStoryViewers, loadMoreStories, loadMoreStoryViewers, @@ -1124,10 +1378,13 @@ export const useSkyPicStore = defineStore('skypic', () => { outgoingRequests, profile, profileAbsentRevision, + publishSpotlight, publishStory, refreshActiveThread, removeFriend, + removeSpotlight, removeStory, + reportSpotlight, replaySnap, resetSession, requests, @@ -1139,6 +1396,13 @@ export const useSkyPicStore = defineStore('skypic', () => { sendMessage, sendSnap, snapOpening, + spotlights, + spotlightsHasMore, + spotlightsLoading, + spotlightsLoadingMore, + spotlightComments, + spotlightCommentsHasMore, + spotlightCommentsLoading, stories, storiesHasMore, storiesLoadingMore, @@ -1152,6 +1416,8 @@ export const useSkyPicStore = defineStore('skypic', () => { threadSnaps, unreadCount, updateProfile, + likeSpotlight, + viewSpotlight, viewedStory, viewStory, } diff --git a/frontend/src/types/skypic.ts b/frontend/src/types/skypic.ts index 74aa2db..853b816 100644 --- a/frontend/src/types/skypic.ts +++ b/frontend/src/types/skypic.ts @@ -146,6 +146,44 @@ 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 = { @@ -178,7 +216,7 @@ export type SkyPicBootstrap = { unreadCount: number } -export type SkyPicDraftPurpose = 'snap' | 'story' +export type SkyPicDraftPurpose = 'snap' | 'spotlight' | 'story' export type SkyPicMediaDraftContext = { purpose: SkyPicDraftPurpose @@ -217,3 +255,11 @@ export type SkyPicSendSnapInput = SkyPicEditorInput & } export type SkyPicPublishStoryInput = SkyPicEditorInput & SkyPicSingleMediaInput + +export type SkyPicPublishSpotlightInput = SkyPicEditorInput & { + adHeadline: string + commentsEnabled: boolean + isSponsored: boolean + mediaId: number + mediaType: 'video' +} diff --git a/frontend/src/views/apps/SkyPicApp.contract.test.ts b/frontend/src/views/apps/SkyPicApp.contract.test.ts index 4f43339..f1139e5 100644 --- a/frontend/src/views/apps/SkyPicApp.contract.test.ts +++ b/frontend/src/views/apps/SkyPicApp.contract.test.ts @@ -16,31 +16,34 @@ const typeSource = readFileSync( ) describe('SkyPic frontend contract', () => { - it('uses Sky UI and exposes the camera-first four-tab shell', () => { + 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('camera')") expect(viewSource).toContain( - "type Tab = 'camera' | 'chats' | 'friends' | 'stories'", + "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.match(/ { + 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"') - expect(viewSource.match(/class="sp-tab sp-tab--monochrome"/g)).toHaveLength( - 3, + expect(viewSource).toContain( + 'class="sp-tab sp-tab--camera sp-tab--monochrome"', ) - expect(viewSource).toContain('color: var(--sp-camera-blue) !important') + 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') @@ -48,22 +51,49 @@ describe('SkyPic frontend contract', () => { .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 without inventing camera filters or Spotlight', () => { + 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(' + @@ -3364,6 +3833,150 @@ onBeforeUnmount(() => { + +

+
+ {{ t('spotlight.comments') }} + +
+
+
+ + + + + + @{{ comment.author.handle }} +

{{ comment.body }}

+ {{ relativeTime(comment.createdAt) }} +
+ +
+ + + {{ + store.spotlightCommentsLoading + ? phone.t('Common.loading') + : phone.t('Common.loadMore') + }} + +
+
+ + + + +

+ {{ t('spotlight.commentsDisabled') }} +

+
+ + + +
+
+ + {{ t('spotlight.report') }} + {{ t('spotlight.reportBody') }} + + +
+
+ +
+ + +
+
+ span { - display: grid; -} - -.sp-camera-header small { - color: rgba(255, 255, 255, 0.62); - font-size: 10px; - font-weight: 800; - letter-spacing: 0.12em; - text-transform: uppercase; -} - -.sp-camera-header h1 { - margin: 2px 0 0; - font-size: 24px; - line-height: 1; + min-height: var(--sky-navbar-height); } .sp-camera-screen :deep(.sky-segmented) { @@ -3890,7 +4474,7 @@ input:focus-visible { width: 72px; height: 72px; border: 3px solid white; - background: rgba(255, 255, 255, 0.18); + background: var(--sky-glass, rgba(50, 50, 50, 0.5)); } .sp-shutter > span { @@ -3898,12 +4482,10 @@ input:focus-visible { width: 58px; height: 58px; place-items: center; + border: 1px solid rgba(255, 255, 255, 0.16); border-radius: 50%; - background: linear-gradient( - 145deg, - var(--sp-camera-blue), - var(--sp-camera-blue-strong) - ); + background: rgba(255, 255, 255, 0.08); + box-shadow: inset 0 1px rgba(255, 255, 255, 0.16); } .sp-content-navbar :deep(.sky-navbar__inner) { @@ -3926,21 +4508,16 @@ input:focus-visible { .sp-camera-screen { position: relative; isolation: isolate; - padding: calc(var(--sky-safe-area-top) + 8px) var(--sky-page-gutter) + padding: var(--sky-safe-area-top) var(--sky-page-gutter) calc(var(--sky-safe-area-bottom) + 82px); gap: 8px; background: radial-gradient( - circle at 12% 4%, - rgba(44, 157, 255, 0.42), - transparent 34% + circle at 50% 0%, + rgba(255, 255, 255, 0.06), + transparent 36% ), - radial-gradient( - circle at 88% 64%, - rgba(5, 105, 210, 0.36), - transparent 40% - ), - linear-gradient(160deg, #06182d 0%, #0b2748 48%, #04111f 100%); + linear-gradient(180deg, #0b0b0b 0%, #000 100%); } .sp-camera-screen::before { @@ -3958,24 +4535,34 @@ input:focus-visible { .sp-camera-header { position: relative; z-index: 3; - display: grid; - grid-template-columns: var(--sky-touch-target) 1fr var(--sky-touch-target); + display: flex; + justify-content: space-between; + gap: var(--sky-space-3); +} + +.sp-camera-header__actions { + display: flex; + align-items: center; gap: var(--sky-space-2); } -.sp-camera-header__title { - justify-items: center; - text-align: center; -} - -.sp-camera-header h1 { - font-size: 21px; - letter-spacing: -0.02em; +.sp-camera-header__glass { + display: grid; + width: var(--sky-touch-target); + height: var(--sky-touch-target); + padding: 0; + overflow: hidden; + place-items: center; + border-color: rgba(255, 255, 255, 0.2); + border-radius: 50%; + background: var(--sky-glass, rgba(50, 50, 50, 0.5)); + color: #fff; + backdrop-filter: blur(18px) saturate(1.18); } .sp-camera-profile { border: 1px solid rgba(255, 255, 255, 0.18); - background: rgba(5, 16, 30, 0.42); + background: rgba(20, 20, 20, 0.68); } .sp-camera-viewfinder { @@ -3989,10 +4576,10 @@ input:focus-visible { background: radial-gradient( circle at 50% 32%, - rgba(49, 161, 255, 0.22), - transparent 27% + rgba(255, 255, 255, 0.05), + transparent 28% ), - linear-gradient(180deg, rgba(12, 33, 57, 0.78), rgba(3, 12, 24, 0.96)); + linear-gradient(180deg, rgba(18, 18, 18, 0.92), rgba(0, 0, 0, 0.98)); box-shadow: inset 0 1px rgba(255, 255, 255, 0.1), 0 26px 60px rgba(0, 7, 19, 0.34); @@ -4030,7 +4617,7 @@ input:focus-visible { padding: 6px 9px; border: 1px solid rgba(255, 255, 255, 0.12); border-radius: var(--sky-radius-pill); - background: rgba(3, 12, 24, 0.34); + background: rgba(0, 0, 0, 0.52); color: rgba(255, 255, 255, 0.76); font-size: 9px; font-weight: 800; @@ -4112,7 +4699,7 @@ input:focus-visible { .sp-camera-dock :deep(.sky-segmented) { width: min(228px, 76%); border: 1px solid rgba(255, 255, 255, 0.12); - background: rgba(4, 14, 28, 0.5); + background: rgba(18, 18, 18, 0.76); } .sp-camera-actions { @@ -4132,7 +4719,8 @@ input:focus-visible { width: 78px; height: 78px; border-width: 4px; - background: rgba(255, 255, 255, 0.08); + border-color: rgba(255, 255, 255, 0.9); + background: var(--sky-glass, rgba(50, 50, 50, 0.5)); box-shadow: 0 10px 26px rgba(0, 0, 0, 0.28); } @@ -4294,7 +4882,8 @@ input:focus-visible { } .sp-thread :deep(.sky-messages) { - display: grid; + display: flex; + flex-direction: column; gap: 8px; } @@ -4321,6 +4910,7 @@ input:focus-visible { .sp-thread-snap { display: flex; align-items: center; + align-self: flex-start; gap: 8px; width: min(78%, 260px); min-height: 58px; @@ -4333,7 +4923,7 @@ input:focus-visible { } .sp-thread-snap--sent { - justify-self: end; + align-self: flex-end; border-color: rgba(90, 108, 255, 0.34); border-radius: 18px 18px 6px 18px; background: var(--sky-app-accent-soft); @@ -4891,10 +5481,6 @@ input:focus-visible { gap: var(--sky-space-2); } -.sp-discovery :deep(.sky-searchbar) { - margin: var(--sky-space-3); -} - .sp-request-actions { display: flex; gap: 4px; @@ -4988,6 +5574,9 @@ input:focus-visible { } .sp-discovery :deep(.sky-searchbar) { + box-sizing: border-box; + width: calc(100% - (var(--sky-space-3) * 2)); + min-width: 0; margin: var(--sky-space-3) var(--sky-space-3) var(--sky-space-2); } @@ -5075,11 +5664,344 @@ input:focus-visible { .sp-bottom-nav--camera :deep(.sky-tabbar__pane) { border-color: rgba(255, 255, 255, 0.13); - background: rgba(3, 12, 24, 0.7); - box-shadow: 0 14px 38px rgba(0, 5, 15, 0.36); + background: rgba(18, 18, 18, 0.78); + box-shadow: 0 14px 38px rgba(0, 0, 0, 0.38); backdrop-filter: blur(22px) saturate(1.18); } +.sp-spotlight-screen { + position: relative; + width: 100%; + min-height: 100%; + overflow: hidden; + background: #020308; + color: #fff; +} + +.sp-spotlight-header { + position: absolute; + z-index: 8; + top: 0; + right: 0; + left: 0; + box-sizing: border-box; + height: calc(var(--sky-safe-area-top) + var(--sky-navbar-height)); + display: grid; + align-items: center; + gap: var(--sky-space-2); + padding: var(--sky-safe-area-top) var(--sky-page-gutter) 0; + grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); +} + +.sp-spotlight-header > strong { + font-size: 19px; + text-align: center; + text-shadow: 0 2px 12px rgba(0, 0, 0, 0.48); +} + +.sp-spotlight-header__actions { + display: flex; + align-items: center; + gap: var(--sky-space-2); +} + +.sp-spotlight-header__actions--right { + justify-content: flex-end; +} + +.sp-spotlight-header__glass { + display: grid; + width: var(--sky-touch-target); + height: var(--sky-touch-target); + padding: 0; + overflow: hidden; + place-items: center; + border-color: rgba(255, 255, 255, 0.2); + border-radius: 50%; + background: var(--sky-glass, rgba(50, 50, 50, 0.5)); + color: #fff; + backdrop-filter: blur(18px) saturate(1.18); +} + +.sp-spotlight-loading, +.sp-spotlight-empty { + position: absolute; + inset: 0; + display: grid; + place-content: center; + padding: 90px var(--sky-space-5) 120px; + text-align: center; +} + +.sp-spotlight-player, +.sp-spotlight-player > video, +.sp-spotlight-shade { + position: absolute; + inset: 0; + width: 100%; + height: 100%; +} + +.sp-spotlight-player { + overflow: hidden; +} + +.sp-spotlight-player > video { + background: #020308; + object-fit: cover; +} + +.sp-spotlight-shade { + pointer-events: none; + background: + linear-gradient(180deg, rgba(0, 0, 0, 0.4), transparent 24%), + linear-gradient(0deg, rgba(0, 0, 0, 0.78), transparent 48%); +} + +.sp-spotlight-overlay { + position: absolute; + z-index: 2; + top: 43%; + right: 76px; + left: 18px; + font-size: clamp(22px, 7vw, 34px); + line-height: 1.08; + overflow-wrap: anywhere; + text-shadow: 0 2px 12px rgba(0, 0, 0, 0.8); +} + +.sp-spotlight-copy { + position: absolute; + z-index: 3; + right: 78px; + bottom: 118px; + left: 17px; + display: grid; + justify-items: start; + gap: 8px; + text-shadow: 0 2px 12px rgba(0, 0, 0, 0.76); +} + +.sp-sponsored-label { + display: inline-flex; + align-items: center; + gap: 5px; + min-height: 27px; + padding: 0 9px; + border: 1px solid rgba(255, 255, 255, 0.42); + border-radius: var(--sky-radius-pill); + background: rgba(9, 12, 22, 0.58); + font-size: 11px; + font-weight: 800; + letter-spacing: 0.04em; + text-transform: uppercase; + backdrop-filter: blur(14px); +} + +.sp-spotlight-author { + display: flex; + align-items: center; + gap: 9px; + min-height: var(--sky-touch-target); + padding: 0; + border: 0; + background: transparent; + color: inherit; + font: inherit; + text-align: left; +} + +.sp-spotlight-author > span:last-child { + display: grid; + gap: 1px; +} + +.sp-spotlight-author small { + color: rgba(255, 255, 255, 0.78); + font-size: 10px; +} + +.sp-spotlight-copy h2, +.sp-spotlight-copy p { + margin: 0; + overflow-wrap: anywhere; +} + +.sp-spotlight-copy h2 { + font-size: 19px; + line-height: 1.15; +} + +.sp-spotlight-copy p { + font-size: 13px; + line-height: 1.42; +} + +.sp-spotlight-actions { + position: absolute; + z-index: 4; + right: 11px; + bottom: 112px; + display: grid; + justify-items: center; + gap: 8px; +} + +.sp-spotlight-actions button, +.sp-spotlight-stat, +.sp-spotlight-pager button { + display: grid; + place-items: center; + min-width: var(--sky-touch-target); + min-height: var(--sky-touch-target); + padding: 5px; + border: 0; + border-radius: var(--sky-radius-pill); + background: rgba(4, 7, 14, 0.46); + color: #fff; + backdrop-filter: blur(15px); +} + +.sp-spotlight-actions button { + font: inherit; +} + +.sp-spotlight-actions svg { + width: 23px; + height: 23px; +} + +.sp-spotlight-actions small, +.sp-spotlight-stat small { + font-size: 10px; + font-weight: 800; +} + +.sp-spotlight-actions .is-active { + color: var(--sp-pink); +} + +.sp-spotlight-pager { + position: absolute; + z-index: 4; + top: 43%; + right: 12px; + display: grid; + gap: 6px; +} + +.sp-spotlight-pager button:disabled { + opacity: 0.28; +} + +.sp-spotlight-sheet { + display: grid; + gap: var(--sky-space-3); + max-height: min(72vh, 620px); + padding: var(--sky-space-4); +} + +.sp-spotlight-sheet > header { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--sky-space-3); +} + +.sp-spotlight-sheet > header > span { + display: grid; + gap: 3px; +} + +.sp-spotlight-sheet > header small { + color: var(--sky-muted); +} + +.sp-spotlight-sheet__scroll { + display: grid; + gap: var(--sky-space-2); + min-height: 80px; + max-height: 42vh; + overflow-y: auto; + overscroll-behavior: contain; +} + +.sp-spotlight-comment { + display: grid; + align-items: start; + gap: 9px; + padding: 10px; + border: 1px solid var(--sky-hairline); + border-radius: var(--sky-radius-control); + background: var(--sky-surface-muted); + grid-template-columns: auto 1fr auto; +} + +.sp-spotlight-comment > span:nth-child(2) { + min-width: 0; +} + +.sp-spotlight-comment p { + margin: 3px 0; + overflow-wrap: anywhere; + font-size: 13px; + line-height: 1.35; +} + +.sp-spotlight-comment small { + color: var(--sky-muted); + font-size: 10px; +} + +.sp-spotlight-comment > button { + display: grid; + place-items: center; + min-width: var(--sky-touch-target); + min-height: var(--sky-touch-target); + border: 0; + background: transparent; + color: var(--sky-danger, #ff3b30); +} + +.sp-spotlight-comment-form { + display: grid; + align-items: end; + gap: var(--sky-space-2); + grid-template-columns: 1fr auto; +} + +.sp-spotlight-comments-disabled { + margin: 0; + color: var(--sky-muted); + font-size: 12px; + text-align: center; +} + +.sp-report-reasons { + display: grid; + gap: 6px; +} + +.sp-report-reasons button { + display: flex; + align-items: center; + justify-content: space-between; + min-height: var(--sky-touch-target); + padding: 0 13px; + border: 1px solid var(--sky-hairline); + border-radius: var(--sky-radius-control); + background: var(--sky-surface-muted); + color: var(--sky-text); + font: inherit; + font-size: 13px; + font-weight: 700; +} + +.sp-report-reasons button.is-active { + border-color: var(--sky-accent); + color: var(--sky-accent); +} + :deep(.sp-tab) { transition: color var(--sky-transition-normal) ease, @@ -5090,11 +6012,6 @@ input:focus-visible { opacity: 0.58; } -:deep(.sp-tab--camera), -:deep(.sp-tab--camera.sky-tab-button--active) { - color: var(--sp-camera-blue) !important; -} - :deep(.sp-tab--monochrome), :deep(.sp-tab--monochrome.sky-tab-button--active) { color: var(--sp-tab-monochrome) !important; @@ -5242,6 +6159,8 @@ input:focus-visible { } .sp-story-reply { + box-sizing: border-box; + width: auto; position: absolute; z-index: 3; right: var(--sky-page-gutter); @@ -5249,6 +6168,15 @@ input:focus-visible { left: var(--sky-page-gutter); } +.sp-story-reply :deep(.sky-messagebar__inner) { + width: 100%; + min-width: 0; +} + +.sp-story-reply .sp-send-button { + flex: 0 0 var(--sky-touch-target); +} + .sp-media-viewer__owner-actions button { display: flex; align-items: center; diff --git a/frontend/testserver/index.cjs b/frontend/testserver/index.cjs index 8898187..284403c 100644 --- a/frontend/testserver/index.cjs +++ b/frontend/testserver/index.cjs @@ -3734,6 +3734,92 @@ const skyPicStoryViewers = new Map([ ], ], ]) +let skyPicSpotlights = [ + { + adHeadline: '', + author: skyPicProfiles[1], + caption: 'Sunset laps through the city.', + commentCount: 2, + commentsEnabled: true, + createdAt: isoTime(-12 * 60_000), + expiresAt: isoTime(29 * 86_400_000), + id: '60000000-0000-4000-8000-000000000001', + isLiked: false, + isOwner: false, + isSponsored: false, + isViewed: false, + likeCount: 128, + mimeType: 'video/mp4', + overlayColor: '#FFFFFF', + textOverlay: 'Los Santos after dark', + url: mockMedia.find((item) => item.id === 2).url, + viewCount: 1432, + }, + { + adHeadline: 'Discover the city after sunset', + author: skyPicProfiles[3], + caption: 'Your next night route starts here.', + commentCount: 0, + commentsEnabled: true, + createdAt: isoTime(-35 * 60_000), + expiresAt: isoTime(6 * 86_400_000), + id: '60000000-0000-4000-8000-000000000002', + isLiked: true, + isOwner: false, + isSponsored: true, + isViewed: true, + likeCount: 84, + mimeType: 'video/mp4', + overlayColor: '#F8FAFC', + textOverlay: '', + url: mockMedia.find((item) => item.id === 6).url, + viewCount: 908, + }, + { + adHeadline: '', + author: skyPicProfiles[0], + caption: 'My first SkyPic Spotlight.', + commentCount: 0, + commentsEnabled: false, + createdAt: isoTime(-75 * 60_000), + expiresAt: isoTime(29 * 86_400_000), + id: '60000000-0000-4000-8000-000000000003', + isLiked: false, + isOwner: true, + isSponsored: false, + isViewed: true, + likeCount: 7, + mimeType: 'video/mp4', + overlayColor: '#FFFFFF', + textOverlay: 'Night drive', + url: mockMedia.find((item) => item.id === 10).url, + viewCount: 61, + }, +] +const skyPicSpotlightComments = new Map([ + [ + '60000000-0000-4000-8000-000000000001', + [ + { + author: skyPicProfiles[2], + body: 'That route looks incredible.', + createdAt: isoTime(-7 * 60_000), + id: '70000000-0000-4000-8000-000000000001', + isOwner: false, + spotlightId: '60000000-0000-4000-8000-000000000001', + }, + { + author: skyPicProfiles[0], + body: 'Adding this to my next drive.', + createdAt: isoTime(-4 * 60_000), + id: '70000000-0000-4000-8000-000000000002', + isOwner: true, + spotlightId: '60000000-0000-4000-8000-000000000001', + }, + ], + ], +]) +const skyPicReportedSpotlightIds = new Set() const skyPicMessages = new Map([ [ '20000000-0000-4000-8000-000000000001', @@ -3814,6 +3900,20 @@ function skyPicStoryView(story) { } } +function skyPicSpotlightVisible(spotlight) { + return ( + !skyPicReportedSpotlightIds.has(spotlight.id) && + (spotlight.isOwner || !skyPicBlockedProfileIds.has(spotlight.author.id)) + ) +} + +function skyPicSpotlightView(spotlight) { + return { + ...spotlight, + author: skyPicSummary(spotlight.author), + } +} + function skyPicUnreadCount() { return skyPicConversations.reduce( (sum, conversation) => sum + Math.max(0, conversation.unreadCount), @@ -8698,6 +8798,266 @@ app.post('/api/:endpoint', (request, response) => { response.json({ success: true }) return } + if (endpoint === 'skypic:spotlight-feed') { + const offset = skyPicOffset(request.body.offset) + if (offset === null) { + response.json({ success: false, error: 'invalid_request' }) + return + } + response.json({ + success: true, + data: skyPicSpotlights + .filter(skyPicSpotlightVisible) + .slice(offset, offset + 12) + .map(skyPicSpotlightView), + }) + return + } + if (endpoint === 'skypic:publish-spotlight') { + if (!skyPicProfile) { + response.json({ success: false, error: 'profile_required' }) + return + } + const media = mockMedia.find( + (item) => + item.id === Number(request.body.mediaId) && item.mediaType === 'video', + ) + if (!media || request.body.mediaType !== 'video') { + response.json({ success: false, error: 'invalid_media_type' }) + return + } + if ( + typeof request.body.isSponsored !== 'boolean' || + typeof request.body.commentsEnabled !== 'boolean' + ) { + response.json({ success: false, error: 'invalid_request' }) + return + } + const caption = skyPicCaption(request.body.caption) + const textOverlay = skyPicTextOverlay(request.body.textOverlay) + if (caption === null || textOverlay === null) { + response.json({ + success: false, + error: caption === null ? 'invalid_caption' : 'invalid_overlay', + }) + return + } + const adHeadline = String(request.body.adHeadline ?? '').trim() + if ( + (request.body.isSponsored && + (adHeadline.length < 3 || [...adHeadline].length > 80)) || + (!request.body.isSponsored && adHeadline) + ) { + response.json({ success: false, error: 'invalid_ad_headline' }) + return + } + const ownActive = skyPicSpotlights.filter((item) => item.isOwner) + if (ownActive.length >= 20) { + response.json({ success: false, error: 'spotlight_limit_reached' }) + return + } + if ( + request.body.isSponsored && + ownActive.filter((item) => item.isSponsored).length >= 3 + ) { + response.json({ success: false, error: 'sponsored_limit_reached' }) + return + } + const spotlight = { + adHeadline: request.body.isSponsored ? adHeadline : '', + author: skyPicProfiles[0], + caption, + commentCount: 0, + commentsEnabled: request.body.commentsEnabled, + createdAt: new Date().toISOString(), + expiresAt: isoTime((request.body.isSponsored ? 7 : 30) * 86_400_000), + id: randomUUID(), + isLiked: false, + isOwner: true, + isSponsored: request.body.isSponsored, + isViewed: true, + likeCount: 0, + mimeType: 'video/mp4', + overlayColor: /^#[0-9a-f]{6}$/i.test(request.body.overlayColor) + ? request.body.overlayColor + : '#ffffff', + textOverlay, + url: media.url, + viewCount: 0, + } + skyPicSpotlights.unshift(spotlight) + skyPicSpotlightComments.set(spotlight.id, []) + response.json({ success: true, data: skyPicSpotlightView(spotlight) }) + return + } + if (endpoint === 'skypic:view-spotlight') { + const spotlight = skyPicSpotlights.find( + (item) => + item.id === String(request.body.spotlightId ?? '') && + skyPicSpotlightVisible(item), + ) + if (!spotlight) { + response.json({ success: false, error: 'spotlight_unavailable' }) + return + } + if (!spotlight.isOwner && !spotlight.isViewed) { + spotlight.isViewed = true + spotlight.viewCount += 1 + } + response.json({ + success: true, + data: { viewCount: spotlight.viewCount }, + }) + return + } + if (endpoint === 'skypic:like-spotlight') { + const spotlight = skyPicSpotlights.find( + (item) => + item.id === String(request.body.spotlightId ?? '') && + skyPicSpotlightVisible(item), + ) + if (!spotlight || typeof request.body.active !== 'boolean') { + response.json({ success: false, error: 'spotlight_unavailable' }) + return + } + if (spotlight.isLiked !== request.body.active) { + spotlight.isLiked = request.body.active + spotlight.likeCount = Math.max( + 0, + spotlight.likeCount + (request.body.active ? 1 : -1), + ) + } + response.json({ + success: true, + data: { + active: spotlight.isLiked, + likeCount: spotlight.likeCount, + }, + }) + return + } + if (endpoint === 'skypic:spotlight-comments') { + const spotlightId = String(request.body.spotlightId ?? '') + const offset = skyPicOffset(request.body.offset) + const spotlight = skyPicSpotlights.find( + (item) => item.id === spotlightId && skyPicSpotlightVisible(item), + ) + if (!spotlight || offset === null) { + response.json({ success: false, error: 'spotlight_unavailable' }) + return + } + response.json({ + success: true, + data: (skyPicSpotlightComments.get(spotlightId) ?? []) + .slice(offset, offset + 50) + .map((comment) => ({ + ...comment, + author: skyPicSummary(comment.author), + isOwner: comment.author.id === skyPicProfile?.id, + })), + }) + return + } + if (endpoint === 'skypic:comment-spotlight') { + const spotlightId = String(request.body.spotlightId ?? '') + const spotlight = skyPicSpotlights.find( + (item) => item.id === spotlightId && skyPicSpotlightVisible(item), + ) + const body = String(request.body.body ?? '').trim() + if (!spotlight) { + response.json({ success: false, error: 'spotlight_unavailable' }) + return + } + if (!spotlight.commentsEnabled) { + response.json({ success: false, error: 'comments_disabled' }) + return + } + if (!body || [...body].length > 500) { + response.json({ success: false, error: 'invalid_comment' }) + return + } + const comment = { + author: skyPicProfiles[0], + body, + createdAt: new Date().toISOString(), + id: randomUUID(), + isOwner: true, + spotlightId, + } + const comments = skyPicSpotlightComments.get(spotlightId) ?? [] + comments.unshift(comment) + skyPicSpotlightComments.set(spotlightId, comments) + spotlight.commentCount += 1 + response.json({ + success: true, + data: { ...comment, author: skyPicSummary(comment.author) }, + }) + return + } + if (endpoint === 'skypic:delete-spotlight-comment') { + const commentId = String(request.body.commentId ?? '') + let removed = false + for (const spotlight of skyPicSpotlights) { + const comments = skyPicSpotlightComments.get(spotlight.id) ?? [] + const comment = comments.find((item) => item.id === commentId) + if (!comment) continue + if (!spotlight.isOwner && comment.author.id !== skyPicProfile?.id) { + response.json({ success: false, error: 'comment_not_found' }) + return + } + skyPicSpotlightComments.set( + spotlight.id, + comments.filter((item) => item.id !== commentId), + ) + spotlight.commentCount = Math.max(0, spotlight.commentCount - 1) + removed = true + break + } + response.json( + removed + ? { success: true } + : { success: false, error: 'comment_not_found' }, + ) + return + } + if (endpoint === 'skypic:remove-spotlight') { + const spotlightId = String(request.body.spotlightId ?? '') + const spotlight = skyPicSpotlights.find((item) => item.id === spotlightId) + if (!spotlight || !spotlight.isOwner) { + response.json({ success: false, error: 'spotlight_unavailable' }) + return + } + skyPicSpotlights = skyPicSpotlights.filter( + (item) => item.id !== spotlightId, + ) + skyPicSpotlightComments.delete(spotlightId) + response.json({ success: true }) + return + } + if (endpoint === 'skypic:report-spotlight') { + const spotlightId = String(request.body.spotlightId ?? '') + const spotlight = skyPicSpotlights.find( + (item) => item.id === spotlightId && skyPicSpotlightVisible(item), + ) + const reasons = new Set([ + 'spam', + 'harassment', + 'dangerous', + 'illegal', + 'other', + ]) + if (!spotlight || spotlight.isOwner) { + response.json({ success: false, error: 'spotlight_unavailable' }) + return + } + if (!reasons.has(request.body.reason)) { + response.json({ success: false, error: 'invalid_report' }) + return + } + skyPicReportedSpotlightIds.add(spotlightId) + response.json({ success: true }) + return + } if (endpoint === 'skypic:thread') { const friendshipId = String(request.body.friendshipId ?? '') if (!skyPicFriends.some((friend) => friend.friendshipId === friendshipId)) { diff --git a/frontend/testserver/smoke.cjs b/frontend/testserver/smoke.cjs index 939b579..50a4db2 100644 --- a/frontend/testserver/smoke.cjs +++ b/frontend/testserver/smoke.cjs @@ -75,6 +75,7 @@ const browserDataRequests = [ ['picstagram:activities', {}], ['skypic:bootstrap', {}], ['skypic:stories', { offset: 0 }], + ['skypic:spotlight-feed', { offset: 0 }], ['radio:get', {}], ['skyride:bootstrap', {}], ['skyride:history', {}], @@ -289,6 +290,23 @@ function verifyBrowserTestData(dataByEndpoint) { dataByEndpoint.get('skypic:stories'), 'SkyPic stories endpoint', ) + const spotlightFeed = dataByEndpoint.get('skypic:spotlight-feed') + expectItems(spotlightFeed, 'SkyPic Spotlight feed', 3) + assert( + spotlightFeed.every( + (spotlight) => + spotlight.mimeType === 'video/mp4' && + typeof spotlight.url === 'string' && + spotlight.url.startsWith('https://'), + ), + 'SkyPic Spotlight feed must contain playable HTTPS videos', + ) + assert( + spotlightFeed.some( + (spotlight) => spotlight.isSponsored && spotlight.adHeadline, + ), + 'SkyPic Spotlight feed must expose clearly labeled sponsored metadata', + ) expectItems(dataByEndpoint.get('radio:get').history, 'radio history', 2) expectItems(dataByEndpoint.get('skyride:history').items, 'SkyRide history', 2) expectItems( @@ -917,6 +935,91 @@ async function verifySkyPicActions(baseUrl) { storyId: publishedStory.id, }) + const spotlightFeed = await expectSuccess( + baseUrl, + 'skypic:spotlight-feed', + { offset: 0 }, + true, + ) + const communitySpotlight = spotlightFeed.find( + (spotlight) => !spotlight.isOwner, + ) + assert(communitySpotlight, 'SkyPic Spotlight community item missing') + const viewedSpotlight = await expectSuccess( + baseUrl, + 'skypic:view-spotlight', + { spotlightId: communitySpotlight.id }, + true, + ) + assert(viewedSpotlight.viewCount >= communitySpotlight.viewCount) + const likedSpotlight = await expectSuccess( + baseUrl, + 'skypic:like-spotlight', + { active: true, spotlightId: communitySpotlight.id }, + true, + ) + assert.equal(likedSpotlight.active, true) + const spotlightComment = await expectSuccess( + baseUrl, + 'skypic:comment-spotlight', + { + body: 'Stateful Spotlight smoke comment', + spotlightId: communitySpotlight.id, + }, + true, + ) + const spotlightComments = await expectSuccess( + baseUrl, + 'skypic:spotlight-comments', + { offset: 0, spotlightId: communitySpotlight.id }, + true, + ) + assert( + spotlightComments.some((comment) => comment.id === spotlightComment.id), + 'SkyPic Spotlight comment did not persist', + ) + await expectSuccess(baseUrl, 'skypic:delete-spotlight-comment', { + commentId: spotlightComment.id, + }) + + const sponsoredSpotlight = await expectSuccess( + baseUrl, + 'skypic:publish-spotlight', + { + adHeadline: 'Drive the skyline tonight', + caption: 'Clearly labeled browser smoke promotion.', + commentsEnabled: true, + durationSeconds: 8, + isSponsored: true, + mediaId: 2, + mediaType: 'video', + overlayColor: '#ffffff', + textOverlay: 'Sponsored route', + }, + true, + ) + assert.equal(sponsoredSpotlight.isSponsored, true) + assert.equal(sponsoredSpotlight.adHeadline, 'Drive the skyline tonight') + await expectSuccess(baseUrl, 'skypic:remove-spotlight', { + spotlightId: sponsoredSpotlight.id, + }) + await expectSuccess(baseUrl, 'skypic:report-spotlight', { + reason: 'spam', + spotlightId: communitySpotlight.id, + }) + const feedAfterReport = await expectSuccess( + baseUrl, + 'skypic:spotlight-feed', + { offset: 0 }, + true, + ) + assert( + !feedAfterReport.some( + (spotlight) => spotlight.id === communitySpotlight.id, + ), + 'reported SkyPic Spotlight remained visible', + ) + const profileUpdate = { allowStoryReplies: false, avatarMediaId: 12, diff --git a/sky_phone/config/config.lua b/sky_phone/config/config.lua index ae142e0..b79a479 100644 --- a/sky_phone/config/config.lua +++ b/sky_phone/config/config.lua @@ -510,10 +510,20 @@ Config.SkyPic = { 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, @@ -525,6 +535,11 @@ Config.SkyPic = { OpensPerMinute = 120, StoriesPerMinute = 6, StoryViewsPerMinute = 120, + SpotlightsPerMinute = 4, + SpotlightViewsPerMinute = 120, + SpotlightReactionsPerMinute = 120, + SpotlightCommentsPerMinute = 20, + SpotlightReportsPerMinute = 10, } Config.Feather = { diff --git a/sky_phone/config/locales/de.lua b/sky_phone/config/locales/de.lua index 04329a3..c044dec 100644 --- a/sky_phone/config/locales/de.lua +++ b/sky_phone/config/locales/de.lua @@ -541,7 +541,7 @@ Locales["de"] = { onboarding = { title = "Willkommen bei SkyPic", eyebrow = "Dein privates Kameranetzwerk", heading = "Teile den Moment", - body = "Erstelle ein Profil für private Snaps, Stories mit Freunden und schnelle Chats.", + 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.", @@ -562,7 +562,8 @@ Locales["de"] = { storyHint = "Teile es 24 Stunden lang in deiner Story.", }, composer = { - snapTitle = "Neuer Snap", storyTitle = "Neue Story", send = "Senden", addStory = "Zur Story hinzufügen", + 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", @@ -570,9 +571,28 @@ Locales["de"] = { 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", + }, }, - tabs = { camera = "Kamera", chats = "Chats", stories = "Stories", friends = "Freunde" }, chats = { title = "Chats", incoming = "Neue Snaps", noSnaps = "Keine ungeöffneten Snaps", conversations = "Unterhaltungen", noConversations = "Deine Unterhaltungen erscheinen hier.", @@ -622,7 +642,7 @@ Locales["de"] = { 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 und Chats werden dauerhaft gelöscht. Dein iFruit-Account und deine Fotomediathek bleiben verfügbar.", + 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" }, @@ -661,6 +681,13 @@ Locales["de"] = { 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.", diff --git a/sky_phone/config/locales/en.lua b/sky_phone/config/locales/en.lua index f17dc5d..2151d90 100644 --- a/sky_phone/config/locales/en.lua +++ b/sky_phone/config/locales/en.lua @@ -541,7 +541,7 @@ Locales["en"] = { onboarding = { title = "Welcome to SkyPic", eyebrow = "Your private camera network", heading = "Share the moment", - body = "Create a profile for private snaps, close-friend stories, and quick chats.", + 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.", @@ -562,7 +562,8 @@ Locales["en"] = { storyHint = "Share it with your story for 24 hours.", }, composer = { - snapTitle = "New snap", storyTitle = "New story", send = "Send", addStory = "Add to story", + 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", @@ -570,9 +571,28 @@ Locales["en"] = { 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", + }, }, - tabs = { camera = "Camera", chats = "Chats", stories = "Stories", friends = "Friends" }, chats = { title = "Chats", incoming = "New snaps", noSnaps = "No unopened snaps", conversations = "Conversations", noConversations = "Your conversations will appear here.", @@ -622,7 +642,7 @@ Locales["en"] = { 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, and chats will be permanently deleted. Your iFruit account and Photos library stay available.", + 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" }, @@ -661,6 +681,13 @@ Locales["en"] = { 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.", diff --git a/sky_phone/source/client/main.lua b/sky_phone/source/client/main.lua index 9feebf2..53ff6a7 100644 --- a/sky_phone/source/client/main.lua +++ b/sky_phone/source/client/main.lua @@ -177,6 +177,15 @@ local server_callbacks = { "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", diff --git a/sky_phone/source/server/db_migrate.lua b/sky_phone/source/server/db_migrate.lua index 5e7bd8d..3a953fa 100644 --- a/sky_phone/source/server/db_migrate.lua +++ b/sky_phone/source/server/db_migrate.lua @@ -3077,6 +3077,111 @@ local schema = { }, 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) diff --git a/sky_phone/source/server/media.lua b/sky_phone/source/server/media.lua index 879ef97..caa4599 100644 --- a/sky_phone/source/server/media.lua +++ b/sky_phone/source/server/media.lua @@ -840,8 +840,12 @@ local function is_referenced_by_skypic(media_id) 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, media_id, media_id }) return rows[1] ~= nil end @@ -862,7 +866,7 @@ local function delete_owned_media(src, owner, media_id) if row.media_type == "photo" and is_required_flare_profile_photo(media_id) then return false, "profile_photo_required" end - -- Both SkyPic foreign keys are RESTRICT. Keep the remote object intact until + -- 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" @@ -882,7 +886,7 @@ local function delete_owned_media(src, owner, media_id) end -- Delete the database parent first and repeat the SkyPic guard inside the -- same statement. The RESTRICT foreign keys then make this atomic against - -- a concurrent snap/story insert: either that insert wins and this DELETE + -- a concurrent snap/story/spotlight insert: either that insert wins and this DELETE -- affects zero rows, or this DELETE wins and the later insert cannot refer -- to a missing media row. local delete_params = {} @@ -891,6 +895,7 @@ local function delete_owned_media(src, owner, media_id) 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 @@ -902,6 +907,10 @@ local function delete_owned_media(src, owner, media_id) 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 diff --git a/sky_phone/source/server/skypic.lua b/sky_phone/source/server/skypic.lua index 48a89c5..1f98b70 100644 --- a/sky_phone/source/server/skypic.lua +++ b/sky_phone/source/server/skypic.lua @@ -1,6 +1,15 @@ Bridge.Database.AfterMigration("sky_phone", function() local settings = Config.SkyPic or {} local json_null = type(json) == "table" and json.null or nil +local spotlight_report_reasons = {} + +for _, reason in ipairs(settings.SpotlightReportReasons or { + "spam", "harassment", "dangerous", "illegal", "other", +}) do + if type(reason) == "string" then + spotlight_report_reasons[reason] = true + end +end local function nullable(value) if value == nil then @@ -563,6 +572,132 @@ local function list_stories(profile_id, offset) return stories end +local function spotlight_from_row(row, profile_id) + if not row then + return nil + end + return { + id = row.id, + author = summary_from_row( + row, + row.profile_id == profile_id and "none" or (row.friendship_id and "friends" or "none"), + row.friendship_id + ), + url = row.url, + mimeType = nullable(row.mime_type), + caption = row.caption or "", + textOverlay = row.overlay_text or "", + overlayColor = row.overlay_color, + isSponsored = row.kind == "sponsored", + adHeadline = row.ad_headline or "", + commentsEnabled = is_true(row.comments_enabled), + expiresAt = row.expires_at, + createdAt = row.created_at, + isOwner = row.profile_id == profile_id, + isLiked = is_true(row.is_liked), + isViewed = is_true(row.is_viewed), + likeCount = tonumber(row.like_count) or 0, + viewCount = tonumber(row.view_count) or 0, + commentCount = tonumber(row.comment_count) or 0, + } +end + +local function list_spotlights(profile_id, offset, spotlight_id) + local exact_id = spotlight_id or "" + local rows = Bridge.Database.Query([[ + SELECT spotlight.`id`, spotlight.`profile_id`, spotlight.`caption`, + spotlight.`overlay_text`, spotlight.`overlay_color`, spotlight.`kind`, + spotlight.`ad_headline`, spotlight.`comments_enabled`, spotlight.`expires_at`, + spotlight.`created_at`, media.`url`, media.`mime_type`, + author.`handle`, author.`display_name`, author.`avatar_seed`, author.`snap_score`, + avatar.`url` AS `avatar_url`, friendship.`id` AS `friendship_id`, + EXISTS( + SELECT 1 FROM `sky_phone_skypic_spotlight_likes` spotlight_like + WHERE spotlight_like.`spotlight_id` = spotlight.`id` + AND spotlight_like.`profile_id` = ? + ) AS `is_liked`, + EXISTS( + SELECT 1 FROM `sky_phone_skypic_spotlight_views` spotlight_view + WHERE spotlight_view.`spotlight_id` = spotlight.`id` + AND spotlight_view.`viewer_profile_id` = ? + ) AS `is_viewed`, + (SELECT COUNT(*) FROM `sky_phone_skypic_spotlight_likes` spotlight_like + WHERE spotlight_like.`spotlight_id` = spotlight.`id`) AS `like_count`, + (SELECT COUNT(*) FROM `sky_phone_skypic_spotlight_views` spotlight_view + WHERE spotlight_view.`spotlight_id` = spotlight.`id`) AS `view_count`, + (SELECT COUNT(*) FROM `sky_phone_skypic_spotlight_comments` comment + WHERE comment.`spotlight_id` = spotlight.`id` + AND comment.`status` = 'visible') AS `comment_count` + FROM `sky_phone_skypic_spotlights` spotlight + JOIN `sky_phone_skypic_profiles` author + ON author.`id` = spotlight.`profile_id` AND author.`status` = 'active' + JOIN `sky_phone_media` media + ON media.`id` = spotlight.`media_id` AND media.`media_type` = 'video' + LEFT JOIN `sky_phone_media` avatar ON avatar.`id` = author.`avatar_media_id` + LEFT JOIN `sky_phone_skypic_friendships` friendship + ON friendship.`profile_a_id` = LEAST(?, spotlight.`profile_id`) + AND friendship.`profile_b_id` = GREATEST(?, spotlight.`profile_id`) + AND friendship.`status` = 'accepted' + WHERE spotlight.`status` = 'active' + AND spotlight.`expires_at` > CURRENT_TIMESTAMP(6) + AND NOT EXISTS ( + SELECT 1 FROM `sky_phone_skypic_blocks` block + WHERE (block.`blocker_profile_id` = ? AND block.`blocked_profile_id` = spotlight.`profile_id`) + OR (block.`blocker_profile_id` = spotlight.`profile_id` AND block.`blocked_profile_id` = ?) + ) + AND NOT EXISTS ( + SELECT 1 FROM `sky_phone_skypic_spotlight_reports` report + WHERE report.`spotlight_id` = spotlight.`id` + AND report.`reporter_profile_id` = ? + ) + AND (? = '' OR spotlight.`id` = ?) + ORDER BY spotlight.`created_at` DESC, spotlight.`id` DESC + LIMIT ? OFFSET ? + ]], { + profile_id, profile_id, profile_id, profile_id, profile_id, profile_id, profile_id, + exact_id, exact_id, spotlight_id and 1 or limit("SpotlightPageSize", 12), offset or 0, + }) + local spotlights = {} + for _, row in ipairs(rows) do + spotlights[#spotlights + 1] = spotlight_from_row(row, profile_id) + end + return spotlights +end + +local function accessible_spotlight(spotlight_id, profile_id) + local rows = Bridge.Database.Query([[ + SELECT spotlight.`id`, spotlight.`profile_id`, spotlight.`comments_enabled` + FROM `sky_phone_skypic_spotlights` spotlight + JOIN `sky_phone_skypic_profiles` author + ON author.`id` = spotlight.`profile_id` AND author.`status` = 'active' + WHERE spotlight.`id` = ? AND spotlight.`status` = 'active' + AND spotlight.`expires_at` > CURRENT_TIMESTAMP(6) + AND NOT EXISTS ( + SELECT 1 FROM `sky_phone_skypic_blocks` block + WHERE (block.`blocker_profile_id` = ? AND block.`blocked_profile_id` = spotlight.`profile_id`) + OR (block.`blocker_profile_id` = spotlight.`profile_id` AND block.`blocked_profile_id` = ?) + ) + LIMIT 1 + ]], { spotlight_id, profile_id, profile_id }) + return rows[1] +end + +local function spotlight_comment_from_row(row, profile_id) + return { + id = row.id, + spotlightId = row.spotlight_id, + body = row.body, + createdAt = row.created_at, + isOwner = row.profile_id == profile_id, + author = summary_from_row( + row, + row.profile_id == profile_id and "none" + or friendship_status(profile_id, row.requested_by_id, row.friendship_status), + row.friendship_id + ), + } +end + local function list_conversations(profile_id) local rows = Bridge.Database.Query([[ SELECT friendship.`id` AS `friendship_id`, @@ -2243,6 +2378,357 @@ Bridge.Callbacks.Register("sky_phone:skypic:remove-story", function(source, data or { success = false, error = "story_unavailable" } end) +Bridge.Callbacks.Register("sky_phone:skypic:spotlight-feed", function(source, data) + if not SkyPhone.AllowOperation(source, "skypic_read", limit("ReadActionsPerMinute", 120), 60) then + return { success = false, error = "rate_limited" } + end + local profile, _, error_response = require_profile(source) + if not profile then + return error_response + end + local offset = type(data) == "table" and (data.offset or 0) or 0 + offset = valid_integer(offset, 0, 100000) + if not offset then + return { success = false, error = "invalid_request" } + end + return { success = true, data = list_spotlights(profile.profile_id, offset) } +end) + +Bridge.Callbacks.Register("sky_phone:skypic:publish-spotlight", function(source, data) + if not SkyPhone.AllowOperation(source, "skypic_spotlight", limit("SpotlightsPerMinute", 4), 60) then + return { success = false, error = "rate_limited" } + end + local profile, _, error_response = require_profile(source) + if not profile then + return error_response + end + local editor, editor_error = editor_payload(source, data, false) + if not editor then + return { success = false, error = editor_error } + end + if editor.mediaType ~= "video" then + return { success = false, error = "invalid_media_type" } + end + if type(data.isSponsored) ~= "boolean" or type(data.commentsEnabled) ~= "boolean" then + return { success = false, error = "invalid_request" } + end + local sponsored = data.isSponsored + if sponsored and settings.AllowSponsoredSpotlights ~= true then + return { success = false, error = "ads_disabled" } + end + local headline = trim(data.adHeadline) + if sponsored then + if not valid_text(headline, 3, limit("SpotlightAdHeadlineMaxLength", 80)) then + return { success = false, error = "invalid_ad_headline" } + end + elseif headline ~= "" then + return { success = false, error = "invalid_ad_headline" } + end + + local maximum_active = limit("MaximumActiveSpotlights", 20) + local maximum_sponsored = limit("MaximumActiveSponsoredSpotlights", 3) + local spotlight_id = new_id() + local ok = Bridge.Database.Transaction({ + { + query = [[ + UPDATE `sky_phone_skypic_profiles` + SET `friend_count` = `friend_count` + WHERE `id` = ? + ]], + params = { profile.profile_id }, + }, + { + query = [[ + INSERT INTO `sky_phone_skypic_spotlights` + (`id`, `profile_id`, `media_id`, `caption`, `overlay_text`, + `overlay_color`, `kind`, `ad_headline`, `comments_enabled`, `expires_at`) + SELECT ?, profile.`id`, ?, ?, ?, ?, ?, ?, ?, + DATE_ADD(CURRENT_TIMESTAMP(6), INTERVAL ? SECOND) + FROM `sky_phone_skypic_profiles` profile + WHERE profile.`id` = ? AND profile.`status` = 'active' + AND ( + SELECT COUNT(*) FROM `sky_phone_skypic_spotlights` + WHERE `profile_id` = profile.`id` AND `status` = 'active' + AND `expires_at` > CURRENT_TIMESTAMP(6) + ) < ? + AND (? = 'organic' OR ( + SELECT COUNT(*) FROM `sky_phone_skypic_spotlights` + WHERE `profile_id` = profile.`id` AND `kind` = 'sponsored' + AND `status` = 'active' AND `expires_at` > CURRENT_TIMESTAMP(6) + ) < ?) + ]], + params = { + spotlight_id, editor.mediaId, editor.caption, editor.textOverlay, editor.overlayColor, + sponsored and "sponsored" or "organic", headline, data.commentsEnabled and 1 or 0, + sponsored and limit("SponsoredSpotlightLifetimeSeconds", 604800) + or limit("SpotlightLifetimeSeconds", 2592000), + profile.profile_id, maximum_active, sponsored and "sponsored" or "organic", + maximum_sponsored, + }, + }, + }) + if not ok then + return { success = false, error = "request_failed" } + end + local spotlight = list_spotlights(profile.profile_id, 0, spotlight_id)[1] + if not spotlight then + local counts = Bridge.Database.Query([[ + SELECT COUNT(*) AS `total`, + SUM(CASE WHEN `kind` = 'sponsored' THEN 1 ELSE 0 END) AS `sponsored` + FROM `sky_phone_skypic_spotlights` + WHERE `profile_id` = ? AND `status` = 'active' + AND `expires_at` > CURRENT_TIMESTAMP(6) + ]], { profile.profile_id }) + local total = tonumber(counts[1] and counts[1].total) or 0 + local sponsored_count = tonumber(counts[1] and counts[1].sponsored) or 0 + if total >= maximum_active then + return { success = false, error = "spotlight_limit_reached" } + end + if sponsored and sponsored_count >= maximum_sponsored then + return { success = false, error = "sponsored_limit_reached" } + end + return { success = false, error = "request_failed" } + end + return { success = true, data = spotlight } +end) + +Bridge.Callbacks.Register("sky_phone:skypic:view-spotlight", function(source, data) + if not SkyPhone.AllowOperation(source, "skypic_spotlight_view", limit("SpotlightViewsPerMinute", 120), 60) then + return { success = false, error = "rate_limited" } + end + local profile, _, error_response = require_profile(source) + if not profile then + return error_response + end + local spotlight_id = type(data) == "table" and data.spotlightId or nil + if not valid_id(spotlight_id) or not accessible_spotlight(spotlight_id, profile.profile_id) then + return { success = false, error = "spotlight_unavailable" } + end + Bridge.Database.Query([[ + INSERT IGNORE INTO `sky_phone_skypic_spotlight_views` + (`spotlight_id`, `viewer_profile_id`) + SELECT `id`, ? FROM `sky_phone_skypic_spotlights` + WHERE `id` = ? AND `profile_id` <> ? AND `status` = 'active' + AND `expires_at` > CURRENT_TIMESTAMP(6) + ]], { profile.profile_id, spotlight_id, profile.profile_id }) + local rows = Bridge.Database.Query([[ + SELECT COUNT(*) AS `count` FROM `sky_phone_skypic_spotlight_views` + WHERE `spotlight_id` = ? + ]], { spotlight_id }) + return { success = true, data = { viewCount = tonumber(rows[1] and rows[1].count) or 0 } } +end) + +Bridge.Callbacks.Register("sky_phone:skypic:like-spotlight", function(source, data) + if not SkyPhone.AllowOperation(source, "skypic_spotlight_reaction", limit("SpotlightReactionsPerMinute", 120), 60) then + return { success = false, error = "rate_limited" } + end + local profile, _, error_response = require_profile(source) + if not profile then + return error_response + end + local spotlight_id = type(data) == "table" and data.spotlightId or nil + local active = type(data) == "table" and data.active or nil + if not valid_id(spotlight_id) or type(active) ~= "boolean" + or not accessible_spotlight(spotlight_id, profile.profile_id) + then + return { success = false, error = "spotlight_unavailable" } + end + if active then + Bridge.Database.Query([[ + INSERT IGNORE INTO `sky_phone_skypic_spotlight_likes` + (`spotlight_id`, `profile_id`) VALUES (?, ?) + ]], { spotlight_id, profile.profile_id }) + else + Bridge.Database.Query([[ + DELETE FROM `sky_phone_skypic_spotlight_likes` + WHERE `spotlight_id` = ? AND `profile_id` = ? + ]], { spotlight_id, profile.profile_id }) + end + local rows = Bridge.Database.Query([[ + SELECT COUNT(*) AS `count` FROM `sky_phone_skypic_spotlight_likes` + WHERE `spotlight_id` = ? + ]], { spotlight_id }) + return { + success = true, + data = { active = active, likeCount = tonumber(rows[1] and rows[1].count) or 0 }, + } +end) + +Bridge.Callbacks.Register("sky_phone:skypic:spotlight-comments", function(source, data) + if not SkyPhone.AllowOperation(source, "skypic_read", limit("ReadActionsPerMinute", 120), 60) then + return { success = false, error = "rate_limited" } + end + local profile, _, error_response = require_profile(source) + if not profile then + return error_response + end + local spotlight_id = type(data) == "table" and data.spotlightId or nil + local offset = type(data) == "table" and (data.offset or 0) or 0 + offset = valid_integer(offset, 0, 100000) + if not valid_id(spotlight_id) or not offset + or not accessible_spotlight(spotlight_id, profile.profile_id) + then + return { success = false, error = "spotlight_unavailable" } + end + local rows = Bridge.Database.Query([[ + SELECT comment.`id`, comment.`spotlight_id`, comment.`profile_id`, comment.`body`, + comment.`created_at`, author.`handle`, author.`display_name`, author.`avatar_seed`, + author.`snap_score`, avatar.`url` AS `avatar_url`, + friendship.`id` AS `friendship_id`, friendship.`status` AS `friendship_status`, + friendship.`requested_by_id` + FROM `sky_phone_skypic_spotlight_comments` comment + JOIN `sky_phone_skypic_profiles` author + ON author.`id` = comment.`profile_id` AND author.`status` = 'active' + LEFT JOIN `sky_phone_media` avatar ON avatar.`id` = author.`avatar_media_id` + LEFT JOIN `sky_phone_skypic_friendships` friendship + ON friendship.`profile_a_id` = LEAST(?, comment.`profile_id`) + AND friendship.`profile_b_id` = GREATEST(?, comment.`profile_id`) + WHERE comment.`spotlight_id` = ? AND comment.`status` = 'visible' + AND NOT EXISTS ( + SELECT 1 FROM `sky_phone_skypic_blocks` block + WHERE (block.`blocker_profile_id` = ? AND block.`blocked_profile_id` = comment.`profile_id`) + OR (block.`blocker_profile_id` = comment.`profile_id` AND block.`blocked_profile_id` = ?) + ) + ORDER BY comment.`created_at` DESC, comment.`id` DESC + LIMIT ? OFFSET ? + ]], { + profile.profile_id, profile.profile_id, spotlight_id, + profile.profile_id, profile.profile_id, + limit("SpotlightCommentPageSize", 50), offset, + }) + local comments = {} + for _, row in ipairs(rows) do + comments[#comments + 1] = spotlight_comment_from_row(row, profile.profile_id) + end + return { success = true, data = comments } +end) + +Bridge.Callbacks.Register("sky_phone:skypic:comment-spotlight", function(source, data) + if not SkyPhone.AllowOperation(source, "skypic_spotlight_comment", limit("SpotlightCommentsPerMinute", 20), 60) then + return { success = false, error = "rate_limited" } + end + local profile, _, error_response = require_profile(source) + if not profile then + return error_response + end + local spotlight_id = type(data) == "table" and data.spotlightId or nil + local body = type(data) == "table" and trim(data.body) or "" + if not valid_id(spotlight_id) + or not valid_text(body, 1, limit("SpotlightCommentMaxLength", 500)) + then + return { success = false, error = "invalid_comment" } + end + local spotlight = accessible_spotlight(spotlight_id, profile.profile_id) + if not spotlight then + return { success = false, error = "spotlight_unavailable" } + end + if not is_true(spotlight.comments_enabled) then + return { success = false, error = "comments_disabled" } + end + local comment_id = new_id() + Bridge.Database.Query([[ + INSERT INTO `sky_phone_skypic_spotlight_comments` + (`id`, `spotlight_id`, `profile_id`, `body`) + SELECT ?, spotlight.`id`, ?, ? + FROM `sky_phone_skypic_spotlights` spotlight + WHERE spotlight.`id` = ? AND spotlight.`status` = 'active' + AND spotlight.`comments_enabled` = 1 + AND spotlight.`expires_at` > CURRENT_TIMESTAMP(6) + ]], { comment_id, profile.profile_id, body, spotlight_id }) + local rows = Bridge.Database.Query([[ + SELECT comment.`id`, comment.`spotlight_id`, comment.`profile_id`, comment.`body`, + comment.`created_at`, author.`handle`, author.`display_name`, author.`avatar_seed`, + author.`snap_score`, avatar.`url` AS `avatar_url` + FROM `sky_phone_skypic_spotlight_comments` comment + JOIN `sky_phone_skypic_profiles` author ON author.`id` = comment.`profile_id` + LEFT JOIN `sky_phone_media` avatar ON avatar.`id` = author.`avatar_media_id` + WHERE comment.`id` = ? AND comment.`profile_id` = ? + LIMIT 1 + ]], { comment_id, profile.profile_id }) + if not rows[1] then + return { success = false, error = "spotlight_unavailable" } + end + return { success = true, data = spotlight_comment_from_row(rows[1], profile.profile_id) } +end) + +Bridge.Callbacks.Register("sky_phone:skypic:delete-spotlight-comment", function(source, data) + if not SkyPhone.AllowOperation(source, "skypic_spotlight_comment", limit("SpotlightCommentsPerMinute", 20), 60) then + return { success = false, error = "rate_limited" } + end + local profile, _, error_response = require_profile(source) + if not profile then + return error_response + end + local comment_id = type(data) == "table" and data.commentId or nil + if not valid_id(comment_id) then + return { success = false, error = "invalid_request" } + end + local result = Bridge.Database.Query([[ + UPDATE `sky_phone_skypic_spotlight_comments` comment + JOIN `sky_phone_skypic_spotlights` spotlight + ON spotlight.`id` = comment.`spotlight_id` + SET comment.`status` = 'removed' + WHERE comment.`id` = ? AND comment.`status` = 'visible' + AND (comment.`profile_id` = ? OR spotlight.`profile_id` = ?) + ]], { comment_id, profile.profile_id, profile.profile_id }) + return affected_rows(result) == 1 and { success = true } + or { success = false, error = "comment_not_found" } +end) + +Bridge.Callbacks.Register("sky_phone:skypic:remove-spotlight", function(source, data) + if not SkyPhone.AllowOperation(source, "skypic_spotlight", limit("SpotlightsPerMinute", 4), 60) then + return { success = false, error = "rate_limited" } + end + local profile, _, error_response = require_profile(source) + if not profile then + return error_response + end + local spotlight_id = type(data) == "table" and data.spotlightId or nil + if not valid_id(spotlight_id) then + return { success = false, error = "invalid_request" } + end + local result = Bridge.Database.Query([[ + UPDATE `sky_phone_skypic_spotlights` SET `status` = 'removed' + WHERE `id` = ? AND `profile_id` = ? AND `status` = 'active' + ]], { spotlight_id, profile.profile_id }) + return affected_rows(result) == 1 and { success = true } + or { success = false, error = "spotlight_unavailable" } +end) + +Bridge.Callbacks.Register("sky_phone:skypic:report-spotlight", function(source, data) + if not SkyPhone.AllowOperation(source, "skypic_spotlight_report", limit("SpotlightReportsPerMinute", 10), 60) then + return { success = false, error = "rate_limited" } + end + local profile, _, error_response = require_profile(source) + if not profile then + return error_response + end + local spotlight_id = type(data) == "table" and data.spotlightId or nil + local reason = type(data) == "table" and trim(data.reason):lower() or "" + local details = type(data) == "table" and trim(data.details) or "" + if not valid_id(spotlight_id) or not spotlight_report_reasons[reason] + or not valid_text(details, 0, 500) + then + return { success = false, error = "invalid_report" } + end + local spotlight = accessible_spotlight(spotlight_id, profile.profile_id) + if not spotlight then + return { success = false, error = "spotlight_unavailable" } + end + if spotlight.profile_id == profile.profile_id then + return { success = false, error = "not_authorized" } + end + Bridge.Database.Query([[ + INSERT INTO `sky_phone_skypic_spotlight_reports` + (`spotlight_id`, `reporter_profile_id`, `reason`, `details`) + VALUES (?, ?, ?, ?) + ON DUPLICATE KEY UPDATE + `reason` = VALUES(`reason`), `details` = VALUES(`details`), + `status` = 'pending', `created_at` = CURRENT_TIMESTAMP(6) + ]], { spotlight_id, profile.profile_id, reason, details }) + return { success = true } +end) + CreateThread(function() while true do Wait(limit("CleanupIntervalSeconds", 45) * 1000) @@ -2259,6 +2745,10 @@ CreateThread(function() DELETE FROM `sky_phone_skypic_stories` WHERE `status` = 'removed' OR `expires_at` <= CURRENT_TIMESTAMP(6) ]], {}) + Bridge.Database.Query([[ + DELETE FROM `sky_phone_skypic_spotlights` + WHERE `status` = 'removed' OR `expires_at` <= CURRENT_TIMESTAMP(6) + ]], {}) Bridge.Database.Query([[ UPDATE `sky_phone_skypic_friendships` SET `streak_count` = 0 diff --git a/sky_phone/sql/install.sql b/sky_phone/sql/install.sql index 4ca59cd..751cdda 100644 --- a/sky_phone/sql/install.sql +++ b/sky_phone/sql/install.sql @@ -1607,3 +1607,73 @@ CREATE TABLE IF NOT EXISTS `sky_phone_skypic_story_views` ( 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;