ENH - open exact EasyShare content targets

This commit is contained in:
smx.pusha
2026-08-12 07:44:55 +02:00
parent eeb3396f52
commit ea34dce1c8
13 changed files with 169 additions and 4 deletions
+8
View File
@@ -183,4 +183,12 @@ describe('FlipTok verification updates', () => {
expect(store.feed).toEqual([])
expect(store.activities).toEqual([])
})
it('loads an exact shared video by id', async () => {
vi.mocked(nuiCall).mockResolvedValue({ success: true, data: { ...video } })
const store = useFlipTokStore()
expect(await store.loadVideo(video.id)).toEqual(video)
expect(nuiCall).toHaveBeenCalledWith('fliptok:video', { id: video.id })
})
})
+4
View File
@@ -114,6 +114,10 @@ export const useFlipTokStore = defineStore('fliptok', {
this.feed = response.data.items
return true
},
async loadVideo(id: string): Promise<FlipTokVideo | null> {
const response = await nuiCall<FlipTokVideo>('fliptok:video', { id })
return response.success && response.data ? response.data : null
},
async discover(search: string): Promise<FlipTokVideo[]> {
const response = await nuiCall<FlipTokVideo[]>('fliptok:discover', {
search,
+8
View File
@@ -213,4 +213,12 @@ describe('Picstagram store', () => {
expect(store.feed).toEqual([])
expect(store.stories).toEqual([])
})
it('loads an exact shared post by id', async () => {
vi.mocked(nuiCall).mockResolvedValue({ success: true, data: { ...post } })
const store = usePicstagramStore()
expect(await store.loadPost(post.id)).toEqual(post)
expect(nuiCall).toHaveBeenCalledWith('picstagram:post', { id: post.id })
})
})
+4
View File
@@ -127,6 +127,10 @@ export const usePicstagramStore = defineStore('picstagram', {
this.feedCursor = response.data.nextCursor
return true
},
async loadPost(id: string): Promise<PicstagramPost | null> {
const response = await nuiCall<PicstagramPost>('picstagram:post', { id })
return response.success && response.data ? response.data : null
},
async loadExplore(append = false): Promise<boolean> {
const response = await nuiCall<PicstagramPage>('picstagram:explore', {
cursor: append ? this.exploreCursor : undefined,
+22
View File
@@ -49,4 +49,26 @@ describe('EasyShare deep links', () => {
).query.listingId,
).toBe('listing-1')
})
it.each([
['picstagram', 'post', 'pic-post-1', '/apps/picstagram'],
['picstagram', 'profile', 'pic-profile-1', '/apps/picstagram'],
['feather', 'post', 'feather-post-1', '/apps/feather'],
['fliptok', 'post', 'fliptok-1', '/apps/fliptok'],
['local-pages', 'post', 'pages-post-1', '/apps/local-pages'],
])('preserves the exact %s %s target', (appId, kind, id, path) => {
expect(
easyShareRoute(
payload({
appId: appId as EasySharePayload['appId'],
id,
kind: kind as EasySharePayload['kind'],
link: `skyphone://${appId}/${kind}/${id}`,
}),
),
).toMatchObject({
path,
query: { easyShareId: id, easyShareKind: kind },
})
})
})
+11
View File
@@ -709,6 +709,17 @@ onMounted(async () => {
if (route.query.compose === '1') screen.value = 'composer'
await feather.bootstrap()
if (feather.onboarded) {
const easyShareId = String(route.query.easyShareId ?? '')
if (easyShareId && route.query.easyShareKind === 'profile') {
const profileId = Number(easyShareId)
if (Number.isInteger(profileId) && profileId > 0)
await openProfile(profileId)
} else if (easyShareId && route.query.easyShareKind === 'post') {
if (await feather.loadThread(easyShareId)) {
threadReplyTarget.value = feather.thread?.post ?? null
screen.value = 'thread'
}
}
await feather.loadActivities()
if (tab.value === 'profile' && feather.profile)
await feather.loadProfile(feather.profile.id)
+14 -1
View File
@@ -50,7 +50,7 @@ import {
kToggle,
} from 'konsta/vue'
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { useRouter } from 'vue-router'
import { useRoute, useRouter } from 'vue-router'
import flipTokIcon from '@/assets/img/app-icons/fliptok.webp'
import { useFlipTokStore } from '@/stores/fliptok'
@@ -71,6 +71,7 @@ type AuthMode = 'login' | 'register'
const phone = usePhoneStore()
const store = useFlipTokStore()
const messageMedia = useMessageMediaStore()
const route = useRoute()
const router = useRouter()
const authMode = ref<AuthMode>('login')
const authHandle = ref('')
@@ -776,6 +777,18 @@ onMounted(async () => {
composeOpen.value = true
}
await store.bootstrap()
const easyShareId = String(route.query.easyShareId ?? '')
if (easyShareId && route.query.easyShareKind === 'profile') {
const profileId = Number(easyShareId)
if (Number.isInteger(profileId) && profileId > 0)
await openProfile(profileId)
} else if (easyShareId && route.query.easyShareKind === 'post') {
const video = await store.loadVideo(easyShareId)
if (video) {
store.feed = [video]
tab.value = 'feed'
}
}
await nextTick()
observeVideos()
})
+11 -2
View File
@@ -279,7 +279,7 @@ function sharePost(): void {
})
}
onMounted(() => {
onMounted(async () => {
const selection = messageMedia.consumeMany<MediaContext>('local-pages:compose')
if (selection) {
if (selection.context) {
@@ -294,7 +294,16 @@ onMounted(() => {
}
}
if (route.query.compose === '1') screen.value = 'compose'
void loadFeed()
await loadFeed()
const easyShareId = String(route.query.easyShareId ?? '')
if (easyShareId && route.query.easyShareKind === 'post') {
const response = await pages.get(easyShareId)
if (response.success && response.data) {
selected.value = response.data
galleryIndex.value = 0
screen.value = 'detail'
}
}
})
</script>
+12 -1
View File
@@ -55,7 +55,7 @@ import {
kToggle,
} from 'konsta/vue'
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { useRouter } from 'vue-router'
import { useRoute, useRouter } from 'vue-router'
import picstagramIcon from '@/assets/img/app-icons/picstagram.webp'
import { useMessageMediaStore } from '@/stores/messageMedia'
@@ -80,6 +80,7 @@ type ProfileSection = 'photos' | 'saved'
const phone = usePhoneStore()
const store = usePicstagramStore()
const mediaPicker = useMessageMediaStore()
const route = useRoute()
const router = useRouter()
const tab = ref<Tab>('home')
@@ -629,6 +630,16 @@ watch(profileSection, (section) => {
onMounted(async () => {
await store.bootstrap()
const easyShareId = String(route.query.easyShareId ?? '')
if (easyShareId && route.query.easyShareKind === 'profile') {
await openProfile(easyShareId)
} else if (easyShareId && route.query.easyShareKind === 'post') {
const post = await store.loadPost(easyShareId)
if (post) {
tab.value = 'home'
selectedPost.value = post
}
}
const composeSelection = mediaPicker.consumeMany<{
caption?: string
commentsEnabled?: boolean
+18
View File
@@ -5387,6 +5387,15 @@ app.post('/api/:endpoint', (request, response) => {
})
return
}
if (endpoint === 'picstagram:post') {
const post = picstagramPosts.find((item) => item.id === request.body.id)
response.json(
post
? { success: true, data: post }
: { success: false, error: 'post_not_found' },
)
return
}
if (endpoint === 'picstagram:explore') {
response.json({
success: true,
@@ -5768,6 +5777,15 @@ app.post('/api/:endpoint', (request, response) => {
response.json({ success: true })
return
}
if (endpoint === 'fliptok:video') {
const video = flipTokVideos.find((item) => item.id === request.body.id)
response.json(
video
? { success: true, data: video }
: { success: false, error: 'video_not_found' },
)
return
}
if (endpoint === 'fliptok:follow') {
flipTokVideos
.filter((video) => video.profile_id === request.body.profileId)
+2
View File
@@ -69,6 +69,7 @@ local server_callbacks = {
"fliptok:logout",
"fliptok:bootstrap",
"fliptok:feed",
"fliptok:video",
"fliptok:discover",
"fliptok:publish",
"fliptok:react",
@@ -91,6 +92,7 @@ local server_callbacks = {
"picstagram:logout",
"picstagram:bootstrap",
"picstagram:feed",
"picstagram:post",
"picstagram:explore",
"picstagram:search",
"picstagram:saved",
+26
View File
@@ -303,6 +303,32 @@ end)
Bridge.Callbacks.Register("sky_phone:fliptok:feed", feed)
Bridge.Callbacks.Register("sky_phone:fliptok:video", function(source, data)
local profile, error_response = require_profile(source)
if not profile then return error_response end
if not SkyPhone.AllowOperation(source, "fliptok:video", 60, 60) then
return { success = false, error = "rate_limited" }
end
local video_id = type(data) == "table" and data.id or nil
if type(video_id) ~= "string" or #video_id > 64 then
return { success = false, error = "video_not_found" }
end
local rows = list_videos(
profile.id,
[[v.`id` = ? AND (v.`visibility` = 'public' OR v.`profile_id` = ? OR
(v.`visibility` = 'followers' AND EXISTS(
SELECT 1 FROM `sky_phone_fliptok_follows` follow
WHERE follow.`follower_id` = ? AND follow.`following_id` = v.`profile_id`)))]],
{ video_id, profile.id, profile.id, profile.id, profile.id },
1,
0,
"v.`created_at` DESC"
)
return rows[1]
and { success = true, data = rows[1] }
or { success = false, error = "video_not_found" }
end)
Bridge.Callbacks.Register("sky_phone:fliptok:discover", function(source, data)
local profile, error_response = require_profile(source)
if not profile then return error_response end
+29
View File
@@ -466,6 +466,35 @@ Bridge.Callbacks.Register("sky_phone:picstagram:feed", function(source, data)
return result and { success = true, data = result } or { success = false, error = list_error }
end)
Bridge.Callbacks.Register("sky_phone:picstagram:post", function(source, data)
local profile, error_response = profile_for_session(source)
if not profile then
return error_response
end
if not SkyPhone.AllowOperation(source, "picstagram:post", 60, 60) then
return { success = false, error = "rate_limited" }
end
local post_id = type(data) == "table" and data.id or nil
if not valid_id(post_id) then
return { success = false, error = "post_not_found" }
end
local result, list_error = list_posts(
profile.id,
[[post.`id` = ? AND (profile.`private` = 0 OR post.`profile_id` = ? OR EXISTS(
SELECT 1 FROM `sky_phone_picstagram_follows` follow
WHERE follow.`follower_id` = ? AND follow.`following_id` = post.`profile_id`
AND follow.`status` = 'accepted'))]],
{ post_id, profile.id, profile.id },
nil
)
if not result then
return { success = false, error = list_error }
end
return result.items[1]
and { success = true, data = result.items[1] }
or { success = false, error = "post_not_found" }
end)
Bridge.Callbacks.Register("sky_phone:picstagram:explore", function(source, data)
local profile, error_response = profile_for_session(source)
if not profile then