fix(birdy): fix all reported issues - feed, follows, comments, reposts, notifications, privacy, DMs, avatars - #37
Merged
Conversation
…15) Six of the reported birdy bugs share one theme: the server work was already done and the UI simply never reached it. This reconnects those paths rather than building anything new. Following tab was permanently empty Feed.tsx branched on `feed === 'following'` and rendered a hardcoded empty state that never consulted `posts`, so correctly-fetched posts were thrown away. store.listFeed already implements the follows join, and Birdy.tsx already requests it. Collapsed to a single length check with a tab-dependent message. The dev stub returned [] for the following tab too, which is presumably how the placeholder survived. Followers / Following lists always blank apiFollowList calls 'sd-phone:birdy:followList', which was never proxied in client/apps/birdy.lua nor registered in server/birdy/init.lua - and no store/action existed. The screen was fully built against a callback that did not exist. Added store.followList (both directions, with the reciprocal follows_you / is_following flags resolved in SQL), actions.followList, and both registrations. Modelled on photogram, which has this same chain. Comment button did nothing The comment ActionButton was rendered with no onClick, and ActionButton stopPropagation()s - so it also swallowed the card's own open handler, making it the one spot on a post guaranteed to do nothing. It now opens the post, where the reply composer already lives. Tapping a commenter's avatar/name did nothing The profile SlideOver was gated on `!openPostId`. Reaching comments always means a post is open, so that gate suppressed it 100% of the time from comments (it worked from the feed, where no post is open). Dropped that clause; the SlideOver is z-30 over the post's z-20, so it layers correctly and closing it returns to the comments. New posts invisible until you switched tabs actions.create only fired TriggerEvent, which is server-LOCAL (a hook for third-party scripts) and reaches no players. There was no broadcast, no polling, and no foreground refetch - and because AppDeck retains the app, closing and reopening does not remount it, so nothing re-ran. Added a TriggerClientEvent broadcast, a client relay, and a nonce the app bumps on that push and on background->foreground (transition only, so opening the app does not double-fetch). Join date freely editable join_label was a client-writable string that updateProfile accepted verbatim, so anyone could set "January 1889". The real created_at column existed but was never selected. It is now read back, the label is derived from it in serializeProfile, updateProfile ignores the field, and the editor shows it as read-only text. Legacy rows fall back to the stored label. Verified: tsc, eslint (0 errors) and vitest all clean. Not exercised against a live server - no FiveM here - so the Lua paths are reasoned through rather than run. Worth a smoke test of the follow lists and the cross-player feed push before merge. Still outstanding on #15, all genuinely unbuilt rather than miswired, and each deserving its own PR: reposts (no table/action at all - the count is hardcoded 0 server-side and the button only flips local state), private accounts (the flag round-trips but no query or UI reads it), notification badges (no seen/read column exists), post fan-out to followers, DM compose (blocked by there being no handle->citizenid resolver), and avatar/banner uploads (no columns, buttons are decorative).
Reposts were entirely client-side: no table, no action, no callback, and serializePost pinned `reposts = 0` for every post. PostCard flipped a local useState, so the count never moved for anyone else and the repost vanished on the next refetch. - phone_birdy_reposts, mirroring phone_birdy_likes (composite PK = idempotent). - store add/remove/isReposted, plus repost_count and a viewer `reposted` flag in POST_SELECT. That makes the viewer cid params #1 AND #2, so all seven POST_SELECT call sites now pass it twice. - actions.toggleRepost on the toggleLike model, notifying the post author on a new repost (never on undo, never for self-reposts), registered through withNotifyPush and proxied on the client. - A 'repost' notification kind, rendered with the Repeat2 glyph. - PostCard reads server truth; Birdy.tsx owns the optimistic flip like it already does for likes. - deleteAccount now clears reposts, both the account's own and others' on its posts. Typecheck and lint clean. Not exercised against a live server.
…compose, avatars (#15) Completes the remaining #15 items, all previously unbuilt. Notification badges phone_birdy_notifications gains a `seen` column (historic rows backfilled as seen so nobody logs in to a giant badge) plus an unseen index. Opening the Bell tab marks everything seen; a notificationCount callback feeds the in-app Bell badge, birdy is registered in the springboard badge snapshot, and SERVER_BADGE_APPS gains 'birdy' so the server owns the app-icon count. The web app finally listens to the existing notification push instead of letting it fire into the void. Post fan-out Posting now writes a 'post' notification for every follower and, for the ones online, sends the app nudge, a phone banner, and a badge push - birdy previously never sent a phone banner for anything. Private accounts (the toggle now does what it says) The All feed hides protected authors from non-followers in SQL, profile post tabs return empty + protected for non-followers, follow lists of a protected account are hidden, and the profile page shows a lock state. Followers and the account itself see everything as before. DM compose dmSend accepts a handle (resolved server-side) and rejects self-DMs; a new dmResolve callback maps handle -> conversation id so the UI can open a thread with someone never messaged. Messages gains a compose button with a people search, and other profiles gain a Message button. Avatars and banners avatar/banner VARCHAR(512) columns, validated with the photogram idiom (http prefix, 512 cap, false clears). serializeAuthor/serializeProfile and every profile-card query now carry them; the previously-dead camera buttons in Edit Profile open the shared MediaPickerSheet; the Avatar component (which already took src) now receives it across the app. Typecheck, lint, tests all clean. Not exercised against a live server.
…-follows # Conflicts: # server/birdy/actions.lua
Github-Samuel
added a commit
that referenced
this pull request
Sep 15, 2026
…s, notifications, privacy, DMs, avatars (#37) * fix(birdy): reconnect the feed, follow lists, comments and join date (#15) Six of the reported birdy bugs share one theme: the server work was already done and the UI simply never reached it. This reconnects those paths rather than building anything new. Following tab was permanently empty Feed.tsx branched on `feed === 'following'` and rendered a hardcoded empty state that never consulted `posts`, so correctly-fetched posts were thrown away. store.listFeed already implements the follows join, and Birdy.tsx already requests it. Collapsed to a single length check with a tab-dependent message. The dev stub returned [] for the following tab too, which is presumably how the placeholder survived. Followers / Following lists always blank apiFollowList calls 'sd-phone:birdy:followList', which was never proxied in client/apps/birdy.lua nor registered in server/birdy/init.lua - and no store/action existed. The screen was fully built against a callback that did not exist. Added store.followList (both directions, with the reciprocal follows_you / is_following flags resolved in SQL), actions.followList, and both registrations. Modelled on photogram, which has this same chain. Comment button did nothing The comment ActionButton was rendered with no onClick, and ActionButton stopPropagation()s - so it also swallowed the card's own open handler, making it the one spot on a post guaranteed to do nothing. It now opens the post, where the reply composer already lives. Tapping a commenter's avatar/name did nothing The profile SlideOver was gated on `!openPostId`. Reaching comments always means a post is open, so that gate suppressed it 100% of the time from comments (it worked from the feed, where no post is open). Dropped that clause; the SlideOver is z-30 over the post's z-20, so it layers correctly and closing it returns to the comments. New posts invisible until you switched tabs actions.create only fired TriggerEvent, which is server-LOCAL (a hook for third-party scripts) and reaches no players. There was no broadcast, no polling, and no foreground refetch - and because AppDeck retains the app, closing and reopening does not remount it, so nothing re-ran. Added a TriggerClientEvent broadcast, a client relay, and a nonce the app bumps on that push and on background->foreground (transition only, so opening the app does not double-fetch). Join date freely editable join_label was a client-writable string that updateProfile accepted verbatim, so anyone could set "January 1889". The real created_at column existed but was never selected. It is now read back, the label is derived from it in serializeProfile, updateProfile ignores the field, and the editor shows it as read-only text. Legacy rows fall back to the stored label. Verified: tsc, eslint (0 errors) and vitest all clean. Not exercised against a live server - no FiveM here - so the Lua paths are reasoned through rather than run. Worth a smoke test of the follow lists and the cross-player feed push before merge. Still outstanding on #15, all genuinely unbuilt rather than miswired, and each deserving its own PR: reposts (no table/action at all - the count is hardcoded 0 server-side and the button only flips local state), private accounts (the flag round-trips but no query or UI reads it), notification badges (no seen/read column exists), post fan-out to followers, DM compose (blocked by there being no handle->citizenid resolver), and avatar/banner uploads (no columns, buttons are decorative). * feat(birdy): persist reposts (#15) Reposts were entirely client-side: no table, no action, no callback, and serializePost pinned `reposts = 0` for every post. PostCard flipped a local useState, so the count never moved for anyone else and the repost vanished on the next refetch. - phone_birdy_reposts, mirroring phone_birdy_likes (composite PK = idempotent). - store add/remove/isReposted, plus repost_count and a viewer `reposted` flag in POST_SELECT. That makes the viewer cid params #1 AND #2, so all seven POST_SELECT call sites now pass it twice. - actions.toggleRepost on the toggleLike model, notifying the post author on a new repost (never on undo, never for self-reposts), registered through withNotifyPush and proxied on the client. - A 'repost' notification kind, rendered with the Repeat2 glyph. - PostCard reads server truth; Birdy.tsx owns the optimistic flip like it already does for likes. - deleteAccount now clears reposts, both the account's own and others' on its posts. Typecheck and lint clean. Not exercised against a live server. * feat(birdy): notification badges, post fan-out, private accounts, DM compose, avatars (#15) Completes the remaining #15 items, all previously unbuilt. Notification badges phone_birdy_notifications gains a `seen` column (historic rows backfilled as seen so nobody logs in to a giant badge) plus an unseen index. Opening the Bell tab marks everything seen; a notificationCount callback feeds the in-app Bell badge, birdy is registered in the springboard badge snapshot, and SERVER_BADGE_APPS gains 'birdy' so the server owns the app-icon count. The web app finally listens to the existing notification push instead of letting it fire into the void. Post fan-out Posting now writes a 'post' notification for every follower and, for the ones online, sends the app nudge, a phone banner, and a badge push - birdy previously never sent a phone banner for anything. Private accounts (the toggle now does what it says) The All feed hides protected authors from non-followers in SQL, profile post tabs return empty + protected for non-followers, follow lists of a protected account are hidden, and the profile page shows a lock state. Followers and the account itself see everything as before. DM compose dmSend accepts a handle (resolved server-side) and rejects self-DMs; a new dmResolve callback maps handle -> conversation id so the UI can open a thread with someone never messaged. Messages gains a compose button with a people search, and other profiles gain a Message button. Avatars and banners avatar/banner VARCHAR(512) columns, validated with the photogram idiom (http prefix, 512 cap, false clears). serializeAuthor/serializeProfile and every profile-card query now carry them; the previously-dead camera buttons in Edit Profile open the shared MediaPickerSheet; the Avatar component (which already took src) now receives it across the app. Typecheck, lint, tests all clean. Not exercised against a live server. --------- Co-authored-by: Samuel Nicol <99494967+Github-Samuel@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes every reported sub-bug in #15, in three commits.
Commit 1 — reconnect what already existed. Birdy's server side was largely complete but unreachable (it appears to have been built from photogram's template with wiring hops dropped):
Feed.tsxrendered a hardcoded empty state that never consultedposts; the server's follows join was already correct.apiFollowListcalled a callback that was never registered anywhere: no client proxy, no server registration, no store query. Added the whole chain, with reciprocalfollowsYou/isFollowingflags resolved in SQL.onClick, and itsstopPropagationalso swallowed the card's open handler. Now opens the post, where the reply composer already lived.!openPostId, which is always false inside comments. Gate dropped; z-order already layers correctly.actions.createfired a server-localTriggerEvent, reaching no players. Added a real broadcast, a client relay, and a refetch nonce (also bumped on background→foreground, since AppDeck never remounts the app).join_labelwas a client-writable string; the realcreated_atwas never selected. Now derived server-side, read-only in the editor.Commit 2 — reposts. Previously pure local state with the count hardcoded
0server-side. Now a realphone_birdy_repoststable mirroring likes, atoggleRepostaction with author notification, viewerrepostedflag in every post query, and optimistic flips owned by the app like likes are.Commit 3 — the remaining unbuilt features.
seencolumn (historic rows backfilled as seen), unseen index, count callback, Bell-tab badge, springboard badge registration, and the web app finally listens to the notification push.dmSendaccepts a handle (the blocker: discovery surfaces never expose citizenids), newdmResolvecallback, compose button with people search, and a Message button on profiles.Verified: tsc, eslint (0 errors), vitest — all clean on every commit. Not exercised against a live FiveM server; the Lua paths are reasoned through, not run. The follow lists, cross-player feed push, badge counts, and DM compose deserve a smoke test before merge.
Migration notes: schema changes are all idempotent (
CREATE TABLE IF NOT EXISTS/ensureColumn), and theseenbackfill only runs when the column is first created.Fixes #15