ADD - implement Feather social app

This commit is contained in:
smx.pusha
2026-08-09 22:09:50 +02:00
parent e7eafb207f
commit 5a4a1beed2
23 changed files with 7642 additions and 10 deletions
+864 -1
View File
@@ -950,6 +950,24 @@ let mockMedia = [
mediaType: 'video',
url: 'https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4',
},
{
createdAt: Date.now() - 180_000,
id: 3,
mediaType: 'photo',
url: 'https://picsum.photos/seed/sky-phone-3/800/600',
},
{
createdAt: Date.now() - 240_000,
id: 4,
mediaType: 'photo',
url: 'https://picsum.photos/seed/sky-phone-4/800/600',
},
{
createdAt: Date.now() - 300_000,
id: 5,
mediaType: 'photo',
url: 'https://picsum.photos/seed/sky-phone-5/800/600',
},
]
const marketplaceInquiries = [
{
@@ -1321,9 +1339,852 @@ function flareBootstrap() {
}
}
const featherProfiles = [
{
id: 1,
handle: 'alexmorgan',
display_name: 'Alex Morgan',
bio: 'City stories, night drives and good coffee.',
avatar_url: 'https://picsum.photos/seed/feather-alex/160/160',
verified: false,
is_owner: true,
is_following: false,
followers: 128,
following: 84,
post_count: 2,
},
{
id: 2,
handle: 'lsdaily',
display_name: 'Los Santos Daily',
bio: 'Independent updates from across the city.',
avatar_url: 'https://picsum.photos/seed/feather-daily/160/160',
verified: true,
is_owner: false,
is_following: true,
followers: 12400,
following: 92,
post_count: 842,
},
{
id: 3,
handle: 'miasantos',
display_name: 'Mia Santos',
bio: 'Music, art and the west side.',
avatar_url: 'https://picsum.photos/seed/feather-mia/160/160',
verified: false,
is_owner: false,
is_following: false,
followers: 834,
following: 310,
post_count: 67,
},
{
id: 4,
handle: 'bennysmotorworks',
display_name: "Benny's Motor Works",
bio: 'Custom builds, workshop stories and open appointments.',
avatar_url: 'https://picsum.photos/seed/feather-bennys/160/160',
verified: true,
is_owner: false,
is_following: true,
followers: 4821,
following: 146,
post_count: 318,
},
{
id: 5,
handle: 'lspd',
display_name: 'Los Santos Police Department',
bio: 'Official public safety updates for Los Santos.',
avatar_url: 'https://picsum.photos/seed/feather-lspd/160/160',
verified: true,
is_owner: false,
is_following: false,
followers: 31700,
following: 41,
post_count: 1294,
},
{
id: 6,
handle: 'nightshiftls',
display_name: 'Night Shift LS',
bio: 'Neon, street photography and stories after midnight.',
avatar_url: 'https://picsum.photos/seed/feather-night/160/160',
verified: false,
is_owner: false,
is_following: false,
followers: 2140,
following: 502,
post_count: 154,
},
{
id: 7,
handle: 'downtowneats',
display_name: 'Downtown Eats',
bio: 'Finding the best food trucks, diners and late-night menus.',
avatar_url: 'https://picsum.photos/seed/feather-food/160/160',
verified: false,
is_owner: false,
is_following: true,
followers: 7602,
following: 288,
post_count: 604,
},
{
id: 8,
handle: 'vinewoodsocial',
display_name: 'Vinewood Social',
bio: 'Events, premieres and everything happening in Vinewood.',
avatar_url: 'https://picsum.photos/seed/feather-vinewood/160/160',
verified: true,
is_owner: false,
is_following: false,
followers: 18900,
following: 73,
post_count: 911,
},
]
let featherFollowingIds = [2, 4, 7]
let featherFollowerIds = [3, 6, 8]
let featherBlockedProfileIds = []
const featherReports = []
let featherPosts = [
{
id: 'feather-post-1',
profile_id: 2,
handle: 'lsdaily',
display_name: 'Los Santos Daily',
verified: true,
avatar_url: featherProfiles[1].avatar_url,
body: 'Golden hour over Downtown. Traffic is building near Legion Square — take the eastern route if you can.',
created_at: Date.now() - 12 * 60 * 1000,
reply_to_id: null,
quote_id: null,
is_owner: false,
is_following: true,
is_liked: true,
is_bookmarked: false,
like_count: 241,
reply_count: 18,
media: [
{
id: 901,
media_type: 'photo',
url: 'https://picsum.photos/seed/feather-city/900/600',
},
],
},
{
id: 'feather-post-2',
profile_id: 3,
handle: 'miasantos',
display_name: 'Mia Santos',
verified: false,
avatar_url: featherProfiles[2].avatar_url,
body: 'Small reminder: the best corners of this city are the ones you find by accident. What is your favorite late-night spot?',
created_at: Date.now() - 48 * 60 * 1000,
reply_to_id: null,
quote_id: null,
is_owner: false,
is_following: false,
is_liked: false,
is_bookmarked: true,
like_count: 73,
reply_count: 9,
media: [],
},
{
id: 'feather-post-3',
profile_id: 1,
handle: 'alexmorgan',
display_name: 'Alex Morgan',
verified: false,
avatar_url: featherProfiles[0].avatar_url,
body: 'First feather. The city feels unusually calm tonight.',
created_at: Date.now() - 3 * 60 * 60 * 1000,
reply_to_id: null,
quote_id: null,
is_owner: true,
is_following: false,
is_liked: false,
is_bookmarked: false,
like_count: 12,
reply_count: 2,
media: [],
},
{
id: 'feather-post-4',
profile_id: 4,
handle: 'bennysmotorworks',
display_name: "Benny's Motor Works",
verified: true,
avatar_url: featherProfiles[3].avatar_url,
body: 'The Elegy is finally ready. New paint, cleaner fitment and a setup made for city nights. #Cars #Bennys',
created_at: Date.now() - 5 * 60 * 60 * 1000,
reply_to_id: null,
quote_id: null,
is_owner: false,
is_following: true,
is_liked: false,
is_bookmarked: false,
like_count: 812,
reply_count: 46,
media: [
{
id: 904,
media_type: 'photo',
url: 'https://picsum.photos/seed/feather-car/900/600',
},
{
id: 905,
media_type: 'photo',
url: 'https://picsum.photos/seed/feather-garage/900/600',
},
],
},
{
id: 'feather-post-5',
profile_id: 5,
handle: 'lspd',
display_name: 'Los Santos Police Department',
verified: true,
avatar_url: featherProfiles[4].avatar_url,
body: 'Road closure: Power Street between Vespucci Boulevard and San Andreas Avenue. Please use an alternate route. #LosSantos #Traffic',
created_at: Date.now() - 7 * 60 * 60 * 1000,
reply_to_id: null,
quote_id: null,
is_owner: false,
is_following: false,
is_liked: false,
is_bookmarked: true,
like_count: 391,
reply_count: 61,
media: [],
},
{
id: 'feather-post-6',
profile_id: 6,
handle: 'nightshiftls',
display_name: 'Night Shift LS',
verified: false,
avatar_url: featherProfiles[5].avatar_url,
body: 'Three quiet frames from tonight. Downtown really changes after the rain. #Photography #NightLife',
created_at: Date.now() - 11 * 60 * 60 * 1000,
reply_to_id: null,
quote_id: null,
is_owner: false,
is_following: false,
is_liked: true,
is_bookmarked: true,
like_count: 1540,
reply_count: 83,
media: [
{
id: 906,
media_type: 'photo',
url: 'https://picsum.photos/seed/feather-neon/900/600',
},
{
id: 907,
media_type: 'photo',
url: 'https://picsum.photos/seed/feather-rain/900/600',
},
{
id: 908,
media_type: 'photo',
url: 'https://picsum.photos/seed/feather-downtown/900/600',
},
],
},
{
id: 'feather-post-7',
profile_id: 7,
handle: 'downtowneats',
display_name: 'Downtown Eats',
verified: false,
avatar_url: featherProfiles[6].avatar_url,
body: 'New taco truck at Legion Square until 2 AM. The hot sauce is not joking around. #Food #LosSantos',
created_at: Date.now() - 16 * 60 * 60 * 1000,
reply_to_id: null,
quote_id: 'feather-post-2',
is_owner: false,
is_following: true,
is_liked: false,
is_bookmarked: false,
like_count: 628,
reply_count: 37,
media: [
{
id: 909,
media_type: 'photo',
url: 'https://picsum.photos/seed/feather-tacos/900/600',
},
],
},
{
id: 'feather-post-8',
profile_id: 8,
handle: 'vinewoodsocial',
display_name: 'Vinewood Social',
verified: true,
avatar_url: featherProfiles[7].avatar_url,
body: 'Outdoor cinema returns to the Vinewood Bowl this weekend. Gates open at 8 PM. #Events #Vinewood',
created_at: Date.now() - 22 * 60 * 60 * 1000,
reply_to_id: null,
quote_id: null,
is_owner: false,
is_following: false,
is_liked: false,
is_bookmarked: false,
like_count: 2203,
reply_count: 104,
media: [
{
id: 910,
media_type: 'photo',
url: 'https://picsum.photos/seed/feather-cinema/900/600',
},
],
},
{
id: 'feather-post-9',
profile_id: 1,
handle: 'alexmorgan',
display_name: 'Alex Morgan',
verified: false,
avatar_url: featherProfiles[0].avatar_url,
body: 'Traffic update for everyone heading downtown later. Drive safe.',
created_at: Date.now() - 26 * 60 * 60 * 1000,
reply_to_id: null,
quote_id: 'feather-post-5',
is_owner: true,
is_following: false,
is_liked: true,
is_bookmarked: false,
like_count: 34,
reply_count: 4,
media: [],
},
{
id: 'feather-reply-1',
profile_id: 1,
handle: 'alexmorgan',
display_name: 'Alex Morgan',
verified: false,
avatar_url: featherProfiles[0].avatar_url,
body: 'Thanks for the warning. The eastern route was much faster.',
created_at: Date.now() - 8 * 60 * 1000,
reply_to_id: 'feather-post-1',
quote_id: null,
is_owner: true,
is_following: false,
is_liked: false,
is_bookmarked: false,
like_count: 5,
reply_count: 0,
media: [],
},
{
id: 'feather-reply-2',
profile_id: 3,
handle: 'miasantos',
display_name: 'Mia Santos',
verified: false,
avatar_url: featherProfiles[2].avatar_url,
body: 'That skyline photo is perfect.',
created_at: Date.now() - 5 * 60 * 1000,
reply_to_id: 'feather-post-1',
quote_id: null,
is_owner: false,
is_following: false,
is_liked: true,
is_bookmarked: false,
like_count: 12,
reply_count: 1,
media: [],
},
]
const featherActivities = [
{
id: 'feather-activity-1',
kind: 'like',
post_id: 'feather-post-3',
profile_id: 3,
handle: 'miasantos',
display_name: 'Mia Santos',
verified: false,
avatar_url: featherProfiles[2].avatar_url,
read: false,
created_at: Date.now() - 15 * 60 * 1000,
},
{
id: 'feather-activity-2',
kind: 'follow',
post_id: null,
profile_id: 2,
handle: 'lsdaily',
display_name: 'Los Santos Daily',
verified: true,
avatar_url: featherProfiles[1].avatar_url,
read: true,
created_at: Date.now() - 90 * 60 * 1000,
},
{
id: 'feather-activity-3',
kind: 'reply',
post_id: 'feather-post-3',
profile_id: 4,
handle: 'bennysmotorworks',
display_name: "Benny's Motor Works",
verified: true,
avatar_url: featherProfiles[3].avatar_url,
read: false,
created_at: Date.now() - 2 * 60 * 60 * 1000,
},
{
id: 'feather-activity-5',
kind: 'quote',
post_id: 'feather-post-3',
profile_id: 6,
handle: 'nightshiftls',
display_name: 'Night Shift LS',
verified: false,
avatar_url: featherProfiles[5].avatar_url,
read: false,
created_at: Date.now() - 9 * 60 * 60 * 1000,
},
]
const featherTopics = [
{ tag: '#LosSantos', count: 8421 },
{ tag: '#Cars', count: 5398 },
{ tag: '#News', count: 4127 },
{ tag: '#NightLife', count: 2944 },
{ tag: '#Food', count: 2186 },
{ tag: '#Vinewood', count: 1773 },
{ tag: '#Photography', count: 1328 },
{ tag: '#Events', count: 986 },
]
let featherOnboarded = true
app.post('/api/:endpoint', (request, response) => {
console.log(`[NUI] ${request.params.endpoint}`, request.body)
const endpoint = request.params.endpoint
const testScenario = String(request.body._testScenario ?? '')
if (endpoint === 'feather:bootstrap') {
const empty = testScenario === 'feather-empty'
const visibleProfiles = featherProfiles.filter(
(profile) => !featherBlockedProfileIds.includes(profile.id),
)
response.json({
success: true,
data: {
onboarded: featherOnboarded,
profile: featherProfiles[0],
feed: {
items: empty
? []
: featherPosts.filter(
(post) =>
!post.reply_to_id &&
!featherBlockedProfileIds.includes(post.profile_id),
),
offset: 0,
hasMore: false,
},
suggestions: empty
? []
: visibleProfiles.filter((profile) => !profile.is_owner),
topics: empty ? [] : featherTopics,
},
})
return
}
if (endpoint === 'feather:create-profile') {
const displayName = String(request.body.displayName ?? '').trim()
const handle = String(request.body.handle ?? '')
.trim()
.toLowerCase()
const bio = String(request.body.bio ?? '').trim()
if (
!displayName ||
displayName.length > 50 ||
!/^[a-z0-9][a-z0-9_]{1,28}[a-z0-9]$/.test(handle) ||
bio.length > 160
) {
response.json({ success: false, error: 'invalid_handle' })
return
}
if (
featherProfiles.some(
(profile) => profile.id !== 1 && profile.handle === handle,
)
) {
response.json({ success: false, error: 'handle_taken' })
return
}
featherProfiles[0].display_name = displayName
featherProfiles[0].handle = handle
featherProfiles[0].bio = bio
featherOnboarded = true
response.json({ success: true })
return
}
if (endpoint === 'feather:update-profile') {
const displayName = String(request.body.displayName ?? '').trim()
const bio = String(request.body.bio ?? '').trim()
if (!displayName || displayName.length > 50 || bio.length > 160) {
response.json({ success: false, error: 'invalid_request' })
return
}
featherProfiles[0].display_name = displayName
featherProfiles[0].bio = bio
featherPosts
.filter((post) => post.profile_id === featherProfiles[0].id)
.forEach((post) => {
post.display_name = displayName
})
response.json({ success: true })
return
}
if (endpoint === 'feather:feed') {
const visiblePosts = featherPosts.filter(
(post) => !featherBlockedProfileIds.includes(post.profile_id),
)
const items =
request.body.mode === 'following'
? visiblePosts.filter(
(post) => !post.reply_to_id && (post.is_following || post.is_owner),
)
: visiblePosts.filter((post) => !post.reply_to_id)
response.json({ success: true, data: { items, offset: 0, hasMore: false } })
return
}
if (endpoint === 'feather:explore') {
const search = String(request.body.search ?? '')
.trim()
.toLowerCase()
const normalizedSearch = search.replace(/^#/, '')
response.json({
success: true,
data: {
posts: featherPosts.filter((post) => {
if (post.reply_to_id) return false
if (featherBlockedProfileIds.includes(post.profile_id)) return false
if (!normalizedSearch) return true
return `${post.body} ${post.display_name} ${post.handle}`
.toLowerCase()
.includes(normalizedSearch)
}),
profiles: featherProfiles
.slice(1)
.filter((profile) => !featherBlockedProfileIds.includes(profile.id))
.filter((profile) =>
normalizedSearch
? `${profile.display_name} ${profile.handle}`
.toLowerCase()
.includes(normalizedSearch)
: true,
),
topics: featherTopics.filter((topic) =>
normalizedSearch
? topic.tag.toLowerCase().includes(normalizedSearch)
: true,
),
},
})
return
}
if (endpoint === 'feather:network') {
const search = String(request.body.search ?? '')
.trim()
.replace(/^@/, '')
.toLowerCase()
const profiles = featherProfiles.filter(
(profile) =>
!profile.is_owner && !featherBlockedProfileIds.includes(profile.id),
)
response.json({
success: true,
data: {
results: search
? profiles.filter((profile) =>
`${profile.display_name} ${profile.handle}`
.toLowerCase()
.includes(search),
)
: [],
suggestions: profiles.filter((profile) => !profile.is_following),
},
})
return
}
if (endpoint === 'feather:create-post') {
const body = String(request.body.body ?? '').trim()
const mediaIds = Array.isArray(request.body.mediaIds)
? request.body.mediaIds.slice(0, 4)
: []
const id = `feather-post-${Date.now()}`
const selectedMedia = mediaIds
.map((mediaId) => mockMedia.find((item) => item.id === Number(mediaId)))
.filter((item) => item?.mediaType === 'photo')
.map((item) => ({
id: item.id,
media_type: 'photo',
url: item.url,
}))
if ((!body && selectedMedia.length === 0) || body.length > 360) {
response.json({ success: false, error: 'invalid_post' })
return
}
const replyToId = request.body.replyToId
? String(request.body.replyToId)
: null
const parent = replyToId
? featherPosts.find((post) => post.id === replyToId)
: null
if (replyToId && !parent) {
response.json({ success: false, error: 'post_not_found' })
return
}
featherPosts.unshift({
id,
profile_id: 1,
handle: featherProfiles[0].handle,
display_name: featherProfiles[0].display_name,
verified: false,
avatar_url: featherProfiles[0].avatar_url,
body,
created_at: Date.now(),
reply_to_id: replyToId,
quote_id: request.body.quoteId ?? null,
is_owner: true,
is_following: false,
is_liked: false,
is_bookmarked: false,
like_count: 0,
reply_count: 0,
media: selectedMedia,
})
if (!replyToId) featherProfiles[0].post_count += 1
if (parent) parent.reply_count += 1
response.json({ success: true, data: { id } })
return
}
if (endpoint === 'feather:react') {
if (!['like', 'bookmark'].includes(request.body.kind)) {
response.json({ success: false, error: 'invalid_request' })
return
}
const post = featherPosts.find((item) => item.id === request.body.id)
if (!post) {
response.json({ success: false, error: 'post_not_found' })
return
}
const key = request.body.kind === 'like' ? 'is_liked' : 'is_bookmarked'
const active = request.body.active === true
if (request.body.kind === 'like' && post.is_liked !== active)
post.like_count = Math.max(0, post.like_count + (active ? 1 : -1))
post[key] = active
response.json({ success: true })
return
}
if (endpoint === 'feather:follow') {
const profileId = Number(request.body.profileId)
const profile = featherProfiles.find((item) => item.id === profileId)
if (!profile || profile.is_owner) {
response.json({ success: false, error: 'profile_not_found' })
return
}
const wasFollowing = featherFollowingIds.includes(profileId)
const active = request.body.active === true
if (active) {
if (!wasFollowing) featherFollowingIds.push(profileId)
} else {
featherFollowingIds = featherFollowingIds.filter((id) => id !== profileId)
}
if (wasFollowing !== active) {
profile.followers = Math.max(0, profile.followers + (active ? 1 : -1))
featherProfiles[0].following = Math.max(
0,
featherProfiles[0].following + (active ? 1 : -1),
)
}
featherProfiles
.filter((item) => item.id === profileId)
.forEach((item) => {
item.is_following = active
})
featherPosts
.filter((item) => item.profile_id === profileId)
.forEach((item) => {
item.is_following = active
})
response.json({ success: true })
return
}
if (endpoint === 'feather:connections') {
const profileId = Number(request.body.profileId ?? 1)
const mode = request.body.mode
let ids = []
if (profileId === 1) {
ids = mode === 'followers' ? featherFollowerIds : featherFollowingIds
} else {
ids = featherProfiles
.filter((profile) => profile.id !== profileId && !profile.is_owner)
.slice(0, 4)
.map((profile) => profile.id)
}
response.json({
success: true,
data: {
items: ids
.map((id) => featherProfiles.find((profile) => profile.id === id))
.filter(Boolean),
},
})
return
}
if (endpoint === 'feather:remove-connection') {
const targetId = Number(request.body.profileId)
if (request.body.mode === 'followers') {
featherFollowerIds = featherFollowerIds.filter((id) => id !== targetId)
featherProfiles[0].followers = Math.max(
0,
featherProfiles[0].followers - 1,
)
} else {
featherFollowingIds = featherFollowingIds.filter((id) => id !== targetId)
featherProfiles[0].following = Math.max(
0,
featherProfiles[0].following - 1,
)
const target = featherProfiles.find((profile) => profile.id === targetId)
if (target) target.is_following = false
featherPosts
.filter((post) => post.profile_id === targetId)
.forEach((post) => {
post.is_following = false
})
}
response.json({ success: true })
return
}
if (endpoint === 'feather:profile') {
const profile =
featherProfiles.find(
(item) => item.id === Number(request.body.profileId),
) ?? featherProfiles[0]
if (featherBlockedProfileIds.includes(profile.id)) {
response.json({ success: false, error: 'profile_not_found' })
return
}
response.json({
success: true,
data: {
profile,
posts: featherPosts.filter((post) => post.profile_id === profile.id),
},
})
return
}
if (endpoint === 'feather:thread') {
const post = featherPosts.find((item) => item.id === request.body.id)
response.json(
post
? {
success: true,
data: {
post,
replies: featherPosts.filter(
(item) =>
item.reply_to_id === post.id &&
!featherBlockedProfileIds.includes(item.profile_id),
),
},
}
: { success: false, error: 'post_not_found' },
)
return
}
if (endpoint === 'feather:activities') {
response.json({
success: true,
data: testScenario === 'feather-empty' ? [] : featherActivities,
})
return
}
if (endpoint === 'feather:mark-activities') {
featherActivities.forEach((item) => {
item.read = true
})
response.json({ success: true })
return
}
if (endpoint === 'feather:delete') {
const post = featherPosts.find((item) => item.id === request.body.id)
if (!post || !post.is_owner) {
response.json({ success: false, error: 'post_not_found' })
return
}
if (post.reply_to_id) {
const parent = featherPosts.find((item) => item.id === post.reply_to_id)
if (parent) parent.reply_count = Math.max(0, parent.reply_count - 1)
}
const deletedIds = new Set(
featherPosts
.filter((item) => item.id === post.id || item.reply_to_id === post.id)
.map((item) => item.id),
)
featherPosts = featherPosts.filter((item) => !deletedIds.has(item.id))
if (!post.reply_to_id)
featherProfiles[0].post_count = Math.max(
0,
featherProfiles[0].post_count - 1,
)
response.json({ success: true })
return
}
if (endpoint === 'feather:block') {
const profileId = Number(request.body.profileId)
const profile = featherProfiles.find((item) => item.id === profileId)
if (!profile || profile.is_owner) {
response.json({ success: false, error: 'profile_not_found' })
return
}
if (!featherBlockedProfileIds.includes(profileId))
featherBlockedProfileIds.push(profileId)
if (featherFollowingIds.includes(profileId)) {
featherFollowingIds = featherFollowingIds.filter((id) => id !== profileId)
featherProfiles[0].following = Math.max(
0,
featherProfiles[0].following - 1,
)
}
profile.is_following = false
response.json({ success: true })
return
}
if (endpoint === 'feather:report') {
const post = featherPosts.find((item) => item.id === request.body.id)
const reason = String(request.body.reason ?? '')
if (
!post ||
!['spam', 'harassment', 'dangerous', 'illegal', 'other'].includes(reason)
) {
response.json({ success: false, error: 'invalid_request' })
return
}
featherReports.push({
id: `feather-report-${Date.now()}`,
post_id: post.id,
reason,
details: String(request.body.details ?? '').slice(0, 500),
created_at: Date.now(),
})
response.json({ success: true })
return
}
if (endpoint === 'radio:get') {
response.json({ success: true, data: radioData })
return
@@ -2190,10 +3051,12 @@ app.post('/api/:endpoint', (request, response) => {
return
}
if (endpoint === 'development:bootstrap') {
if (testScenario === 'feather-onboarding') featherOnboarded = false
else featherOnboarded = true
response.json({
success: true,
data: {
account: linkedAccount,
account: testScenario === 'feather-login' ? null : linkedAccount,
device: {
data: deviceData,
imei: '356938035643809',