Skip to content

fix(birdy): fix all reported issues - feed, follows, comments, reposts, notifications, privacy, DMs, avatars - #37

Merged
Github-Samuel merged 4 commits into
mainfrom
fix/15-birdy-feed-and-follows
Jul 20, 2026
Merged

Github-Samuel merged 4 commits into
mainfrom
fix/15-birdy-feed-and-follows

Conversation

@Epixx1337

@Epixx1337 Epixx1337 commented Jul 18, 2026 •

Copy link
Copy Markdown
Collaborator

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):

  • Following tab always empty — Feed.tsx rendered a hardcoded empty state that never consulted posts; the server's follows join was already correct.
  • Followers/Following lists blank — apiFollowList called a callback that was never registered anywhere: no client proxy, no server registration, no store query. Added the whole chain, with reciprocal followsYou/isFollowing flags resolved in SQL.
  • Comment button dead — rendered with no onClick, and its stopPropagation also swallowed the card's open handler. Now opens the post, where the reply composer already lived.
  • Comment authors unclickable — the profile overlay was gated on !openPostId, which is always false inside comments. Gate dropped; z-order already layers correctly.
  • Posts invisible until tab-switch — actions.create fired a server-local TriggerEvent, 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 date freely editable — join_label was a client-writable string; the real created_at was never selected. Now derived server-side, read-only in the editor.

Commit 2 — reposts. Previously pure local state with the count hardcoded 0 server-side. Now a real phone_birdy_reposts table mirroring likes, a toggleRepost action with author notification, viewer reposted flag in every post query, and optimistic flips owned by the app like likes are.

Commit 3 — the remaining unbuilt features.

  • Notification badges — seen column (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.
  • Post fan-out — followers get a persisted notification and, when online, an app nudge + phone banner + badge push.
  • Private accounts enforced — protected authors hidden from non-followers in the All feed (SQL), profile tabs, and follow lists, with a lock state on the profile page. The toggle no longer lies.
  • DM compose — dmSend accepts a handle (the blocker: discovery surfaces never expose citizenids), new dmResolve callback, compose button with people search, and a Message button on profiles.
  • Avatars/banners — new validated columns, the dead camera buttons now open the shared MediaPickerSheet, and avatars render across posts, profiles, DMs, search, and notifications.

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 the seen backfill only runs when the column is first created.

Fixes #15

…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.
@Epixx1337 Epixx1337 changed the title fix(birdy): reconnect the feed, follow lists, comments and join date fix(birdy): fix all reported issues - feed, follows, comments, reposts, notifications, privacy, DMs, avatars Jul 18, 2026
…-follows

# Conflicts:
#	server/birdy/actions.lua
@Github-Samuel
Github-Samuel merged commit dc2e44d into main Jul 20, 2026
1 check passed
@Github-Samuel
Github-Samuel deleted the fix/15-birdy-feed-and-follows branch July 20, 2026 17:38
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

birdy app

2 participants