mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-08-30 09:48:57 +00:00
ENH - expose Feather bookmarks on profiles
This commit is contained in:
@@ -129,6 +129,36 @@ describe('Feather store', () => {
|
||||
expect(store.connections).toEqual([profile])
|
||||
})
|
||||
|
||||
it('loads bookmarked posts for the owner profile', async () => {
|
||||
const bookmarkedPost = { ...post, is_bookmarked: true }
|
||||
vi.mocked(nuiCall).mockResolvedValue({
|
||||
success: true,
|
||||
data: { items: [bookmarkedPost] },
|
||||
})
|
||||
const store = useFeatherStore()
|
||||
|
||||
expect(await store.loadBookmarks()).toBe(true)
|
||||
expect(nuiCall).toHaveBeenCalledWith('feather:bookmarks')
|
||||
expect(store.bookmarkedPosts).toEqual([bookmarkedPost])
|
||||
expect(store.bookmarksLoading).toBe(false)
|
||||
})
|
||||
|
||||
it('removes an unbookmarked post from the saved profile tab', async () => {
|
||||
vi.mocked(nuiCall).mockResolvedValue({ success: true })
|
||||
const store = useFeatherStore()
|
||||
const bookmarkedPost = { ...post, is_bookmarked: true }
|
||||
store.bookmarkedPosts = [bookmarkedPost]
|
||||
|
||||
await store.react(bookmarkedPost, 'bookmark')
|
||||
|
||||
expect(store.bookmarkedPosts).toEqual([])
|
||||
expect(nuiCall).toHaveBeenCalledWith('feather:react', {
|
||||
active: false,
|
||||
id: bookmarkedPost.id,
|
||||
kind: 'bookmark',
|
||||
})
|
||||
})
|
||||
|
||||
it('removes an owned connection and updates the profile count', async () => {
|
||||
vi.mocked(nuiCall).mockResolvedValue({ success: true })
|
||||
const store = useFeatherStore()
|
||||
|
||||
@@ -17,6 +17,8 @@ export type FeatherConnectionMode = 'followers' | 'following'
|
||||
export const useFeatherStore = defineStore('feather', {
|
||||
state: () => ({
|
||||
activities: [] as FeatherActivity[],
|
||||
bookmarkedPosts: [] as FeatherPost[],
|
||||
bookmarksLoading: false,
|
||||
connectionLoading: false,
|
||||
connections: [] as FeatherProfile[],
|
||||
exploreLoading: false,
|
||||
@@ -135,7 +137,14 @@ export const useFeatherStore = defineStore('feather', {
|
||||
id: post.id,
|
||||
kind,
|
||||
})
|
||||
if (response.success) return
|
||||
if (response.success) {
|
||||
if (kind === 'bookmark') {
|
||||
this.bookmarkedPosts = next
|
||||
? [post, ...this.bookmarkedPosts.filter((item) => item.id !== post.id)]
|
||||
: this.bookmarkedPosts.filter((item) => item.id !== post.id)
|
||||
}
|
||||
return
|
||||
}
|
||||
post[key] = !next
|
||||
if (kind === 'like') post.like_count += next ? -1 : 1
|
||||
},
|
||||
@@ -163,6 +172,7 @@ export const useFeatherStore = defineStore('feather', {
|
||||
...this.feed,
|
||||
...this.explorePosts,
|
||||
...this.profilePosts,
|
||||
...this.bookmarkedPosts,
|
||||
]) {
|
||||
if (post.profile_id === profile.id) post.is_following = next
|
||||
}
|
||||
@@ -178,6 +188,7 @@ export const useFeatherStore = defineStore('feather', {
|
||||
...this.feed,
|
||||
...this.explorePosts,
|
||||
...this.profilePosts,
|
||||
...this.bookmarkedPosts,
|
||||
]) {
|
||||
if (item.profile_id === post.profile_id) item.is_following = next
|
||||
}
|
||||
@@ -222,6 +233,7 @@ export const useFeatherStore = defineStore('feather', {
|
||||
...this.feed,
|
||||
...this.explorePosts,
|
||||
...this.profilePosts,
|
||||
...this.bookmarkedPosts,
|
||||
]) {
|
||||
if (post.profile_id === target.id) post.is_following = false
|
||||
}
|
||||
@@ -237,6 +249,16 @@ export const useFeatherStore = defineStore('feather', {
|
||||
this.profilePosts = response.data.posts
|
||||
return true
|
||||
},
|
||||
async loadBookmarks(): Promise<boolean> {
|
||||
this.bookmarksLoading = true
|
||||
const response = await nuiCall<{ items: FeatherPost[] }>(
|
||||
'feather:bookmarks',
|
||||
)
|
||||
this.bookmarksLoading = false
|
||||
if (!response.success || !response.data) return false
|
||||
this.bookmarkedPosts = response.data.items
|
||||
return true
|
||||
},
|
||||
async loadThread(id: string): Promise<boolean> {
|
||||
const response = await nuiCall<FeatherThread>('feather:thread', { id })
|
||||
if (!response.success || !response.data) return false
|
||||
@@ -258,6 +280,9 @@ export const useFeatherStore = defineStore('feather', {
|
||||
this.feed = this.feed.filter((post) => post.id !== id)
|
||||
this.explorePosts = this.explorePosts.filter((post) => post.id !== id)
|
||||
this.profilePosts = this.profilePosts.filter((post) => post.id !== id)
|
||||
this.bookmarkedPosts = this.bookmarkedPosts.filter(
|
||||
(post) => post.id !== id,
|
||||
)
|
||||
return true
|
||||
},
|
||||
async blockProfile(profileId: number): Promise<boolean> {
|
||||
@@ -267,6 +292,9 @@ export const useFeatherStore = defineStore('feather', {
|
||||
this.explorePosts = this.explorePosts.filter(
|
||||
(post) => post.profile_id !== profileId,
|
||||
)
|
||||
this.bookmarkedPosts = this.bookmarkedPosts.filter(
|
||||
(post) => post.profile_id !== profileId,
|
||||
)
|
||||
this.networkResults = this.networkResults.filter(
|
||||
(profile) => profile.id !== profileId,
|
||||
)
|
||||
|
||||
@@ -548,6 +548,8 @@ const defaultLocales: LocaleTree = {
|
||||
noReplies: 'No replies yet. Start the conversation.',
|
||||
likes: 'Likes',
|
||||
bookmarks: 'Bookmarks',
|
||||
noBookmarks: 'No saved posts yet',
|
||||
noBookmarksBody: 'Posts you bookmark will appear here.',
|
||||
delete: 'Delete',
|
||||
report: 'Report',
|
||||
block: 'Block @{handle}',
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
AlignLeft,
|
||||
AtSign,
|
||||
Bell,
|
||||
Bookmark,
|
||||
Camera,
|
||||
CalendarDays,
|
||||
CheckCircle2,
|
||||
@@ -78,7 +79,7 @@ type Screen =
|
||||
| 'connections'
|
||||
type ExploreView = 'explore' | 'trending' | 'news'
|
||||
type ActivityView = 'all' | 'mentions'
|
||||
type ProfileView = 'posts' | 'replies' | 'media'
|
||||
type ProfileView = 'posts' | 'replies' | 'media' | 'bookmarks'
|
||||
type SelectedPhoto = { id: number; url: string }
|
||||
type ComposerContext = {
|
||||
body: string
|
||||
@@ -141,6 +142,7 @@ const displayedActivities = computed(() =>
|
||||
: feather.activities,
|
||||
)
|
||||
const displayedProfilePosts = computed(() => {
|
||||
if (profileView.value === 'bookmarks') return feather.bookmarkedPosts
|
||||
if (profileView.value === 'replies')
|
||||
return feather.profilePosts.filter((post) => Boolean(post.reply_to_id))
|
||||
if (profileView.value === 'media')
|
||||
@@ -354,10 +356,16 @@ async function selectTab(next: Tab): Promise<void> {
|
||||
await feather.markActivities()
|
||||
}
|
||||
if (next === 'profile' && feather.profile) {
|
||||
profileView.value = 'posts'
|
||||
await feather.loadProfile(feather.profile.id)
|
||||
}
|
||||
}
|
||||
|
||||
async function selectProfileView(next: ProfileView): Promise<void> {
|
||||
profileView.value = next
|
||||
if (next === 'bookmarks') await feather.loadBookmarks()
|
||||
}
|
||||
|
||||
async function setFeedMode(mode: 'for-you' | 'following'): Promise<void> {
|
||||
await feather.loadFeed(mode)
|
||||
}
|
||||
@@ -518,7 +526,10 @@ async function publishThreadReply(): Promise<void> {
|
||||
}
|
||||
|
||||
async function openProfile(profileId: number): Promise<void> {
|
||||
if (await feather.loadProfile(profileId)) screen.value = 'profile'
|
||||
if (await feather.loadProfile(profileId)) {
|
||||
profileView.value = 'posts'
|
||||
screen.value = 'profile'
|
||||
}
|
||||
}
|
||||
|
||||
async function followProfile(profile: FeatherProfile): Promise<void> {
|
||||
@@ -1391,7 +1402,7 @@ onMounted(async () => {
|
||||
type="button"
|
||||
:active="profileView === 'posts'"
|
||||
:class="{ 'is-active': profileView === 'posts' }"
|
||||
@click="profileView = 'posts'"
|
||||
@click="selectProfileView('posts')"
|
||||
>
|
||||
<AlignLeft :size="16" /> {{ t('posts') }}
|
||||
</kSegmentedButton>
|
||||
@@ -1399,7 +1410,7 @@ onMounted(async () => {
|
||||
type="button"
|
||||
:active="profileView === 'replies'"
|
||||
:class="{ 'is-active': profileView === 'replies' }"
|
||||
@click="profileView = 'replies'"
|
||||
@click="selectProfileView('replies')"
|
||||
>
|
||||
<AtSign :size="17" />
|
||||
</kSegmentedButton>
|
||||
@@ -1407,13 +1418,41 @@ onMounted(async () => {
|
||||
type="button"
|
||||
:active="profileView === 'media'"
|
||||
:class="{ 'is-active': profileView === 'media' }"
|
||||
@click="profileView = 'media'"
|
||||
@click="selectProfileView('media')"
|
||||
>
|
||||
<Video :size="17" />
|
||||
</kSegmentedButton>
|
||||
<kSegmentedButton
|
||||
v-if="activeProfile.is_owner"
|
||||
type="button"
|
||||
:active="profileView === 'bookmarks'"
|
||||
:class="{ 'is-active': profileView === 'bookmarks' }"
|
||||
:aria-label="t('bookmarks')"
|
||||
:title="t('bookmarks')"
|
||||
@click="selectProfileView('bookmarks')"
|
||||
>
|
||||
<Bookmark :size="17" />
|
||||
</kSegmentedButton>
|
||||
</kSegmented>
|
||||
<div
|
||||
v-if="profileView === 'bookmarks' && feather.bookmarksLoading"
|
||||
class="feather-network-loading"
|
||||
>
|
||||
<kPreloader />
|
||||
</div>
|
||||
<div
|
||||
v-else-if="
|
||||
profileView === 'bookmarks' && !displayedProfilePosts.length
|
||||
"
|
||||
class="feather-empty"
|
||||
>
|
||||
<Bookmark :size="34" />
|
||||
<h2>{{ t('noBookmarks') }}</h2>
|
||||
<p>{{ t('noBookmarksBody') }}</p>
|
||||
</div>
|
||||
<FeatherPostCard
|
||||
v-for="post in displayedProfilePosts"
|
||||
v-show="profileView !== 'bookmarks' || !feather.bookmarksLoading"
|
||||
:key="post.id"
|
||||
:post="post"
|
||||
@follow="feather.followPost"
|
||||
|
||||
@@ -2673,6 +2673,17 @@ app.post('/api/:endpoint', (request, response) => {
|
||||
})
|
||||
return
|
||||
}
|
||||
if (endpoint === 'feather:bookmarks') {
|
||||
response.json({
|
||||
success: true,
|
||||
data: {
|
||||
items: featherPosts.filter((post) => post.is_bookmarked),
|
||||
offset: 0,
|
||||
hasMore: false,
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
if (endpoint === 'feather:thread') {
|
||||
const post = featherPosts.find((item) => item.id === request.body.id)
|
||||
response.json(
|
||||
|
||||
@@ -211,7 +211,7 @@ Locales["en"] = {
|
||||
trendKinds = { explore = "Trending in Los Santos", trending = "Popular right now", news = "News · Trending", sports = "Sports · Trending", entertainment = "Entertainment · Trending" },
|
||||
people = "Who to follow", posts = "Posts", followers = "Followers", followingCount = "Following", removeConnection = "Remove", followerRemoved = "Follower removed.", followingRemoved = "Account unfollowed.", noConnections = "Nobody here yet", noConnectionsBody = "Profiles will appear here as connections are made.", follow = "Follow", unfollow = "Following",
|
||||
editProfile = "Edit profile", saveProfile = "Save", joined = "Joined Feather", joinedDate = "Joined August 2026", shareProfile = "Share profile", profileCopied = "Profile handle copied.", postCopied = "Post copied.", verified = "Verified profile", noBio = "No bio yet.", showMore = "Show more",
|
||||
replies = "Replies", noReplies = "No replies yet. Start the conversation.", likes = "Likes", bookmarks = "Bookmarks", delete = "Delete", report = "Report", block = "Block @{handle}",
|
||||
replies = "Replies", noReplies = "No replies yet. Start the conversation.", likes = "Likes", bookmarks = "Bookmarks", noBookmarks = "No saved posts yet", noBookmarksBody = "Posts you bookmark will appear here.", delete = "Delete", report = "Report", block = "Block @{handle}",
|
||||
reportTitle = "Report feather", reportBody = "Tell us why this feather should be reviewed.", reportSubmit = "Submit report", reported = "Report submitted.", blocked = "Profile blocked.", deleted = "Feather deleted.",
|
||||
reasons = { spam = "Spam", harassment = "Harassment", dangerous = "Dangerous content", illegal = "Illegal content", other = "Other" },
|
||||
noActivity = "No activity yet", activityBody = "Likes, replies and new followers will appear here.",
|
||||
|
||||
@@ -124,6 +124,7 @@ local server_callbacks = {
|
||||
"feather:connections",
|
||||
"feather:remove-connection",
|
||||
"feather:profile",
|
||||
"feather:bookmarks",
|
||||
"feather:thread",
|
||||
"feather:activities",
|
||||
"feather:mark-activities",
|
||||
|
||||
@@ -552,6 +552,19 @@ Bridge.Callbacks.Register("sky_phone:feather:profile", function(source, data)
|
||||
} }
|
||||
end)
|
||||
|
||||
Bridge.Callbacks.Register("sky_phone:feather:bookmarks", function(source)
|
||||
local profile, error_response = require_profile(source)
|
||||
if not profile then return error_response end
|
||||
local bookmarked = list_posts(
|
||||
tonumber(profile.id),
|
||||
"EXISTS(SELECT 1 FROM `sky_phone_feather_reactions` saved WHERE saved.`profile_id` = ? AND saved.`post_id` = fp.`id` AND saved.`kind` = 'bookmark')",
|
||||
{ profile.id },
|
||||
"fp.`created_at` DESC",
|
||||
0
|
||||
)
|
||||
return { success = true, data = bookmarked }
|
||||
end)
|
||||
|
||||
Bridge.Callbacks.Register("sky_phone:feather:thread", function(source, data)
|
||||
local profile, error_response = require_profile(source)
|
||||
if not profile then return error_response end
|
||||
|
||||
Reference in New Issue
Block a user