Skip to content

Social & services apps ​

Chirp, Yellow Pages, Maps, Calendar, Stocks, Transit (home screen) and News (App Store). Config: shared/config/social.lua (Config.Social). Tables: sql/social.sql (created automatically). Server: server/apps/{social,chirp,yp,maps,calendar,stocks,transit,news}.lua (social.lua holds shared helpers Phone.Social.*: charge/pay with wallet fallback, admin/job checks, URL validation, per-key money locks). Client: client/apps/maps.lua (client RPC maps.distances). Web: web/src/apps/<id>/.

Admin actions (deleting other people's chirps / ads) need the ace np_phone.admin: add_ace group.admin np_phone.admin allow.

Money goes through Phone.Wallet.charge (city team, logs a transaction) when present, otherwise Phone.fw('removeMoney', src, 'bank', …). Payouts (stock sales) use Phone.fw('addMoney', …) plus Phone.Wallet.log.

Chirp (chirp.*, accent #e0423b) ​

An account is created from the character name on first open. The handle can be changed once. Posts starting with #tip are shown as "Anonymous Tip" (Config.Social.chirp.anonymousTips); the author still sees and can delete them, admins see the real author. Verified badge: manual verified column, or the player's job is in Config.Social.verifiedJobs (refreshed whenever the player uses Chirp).

RPCparams → result
chirp.me→ { account, stats, handleLocked, notify, unread, admin, maxLength, maxImages }
chirp.updateProfile{ displayName?, avatar? ('' clears), bio?, handle? (once), notify? } → { account, handleLocked, notify }
chirp.profile{ id } or { handle } → { account, stats, following, me }
chirp.follow{ id, follow } → { following, followers }
chirp.feed{ tab: 'foryou'|'following'|'trending', cursor? } → { posts, cursor } (cursor = last id; trending uses an offset)
chirp.userPosts{ id, tab: 'posts'|'replies'|'likes', cursor? } → { posts, cursor }
chirp.thread{ id } → { post, parent?, replies }
chirp.hashtag{ tag, cursor? } → { posts, cursor, count }
chirp.trends→ { tag, count }[] (last trendingHours)
chirp.search{ q } → { accounts, posts }
chirp.activity→ { items } (likes, replies, mentions, follows, reposts; marks them read)
chirp.post{ text ≤ 280, images?: url[] ≤ 4, replyTo? } → post
chirp.delete{ id } → true (own posts, or admin)
chirp.like / chirp.repost{ id, value } → { value, count }

Push: chirp:new (post, sent to every player except the author; the NUI shows a "new Chirps" pill), chirp:deleted { id }, chirp:activity { type, postId }. Notifications (mentions, replies, likes, reposts, follows) via Phone.notify, only when the account's notify setting is on. NUI deep links: openApp('chirp', { postId } | { handle } | { compose } | { attachImage }) (attachImage: Photos share sheet → composer with the image attached). Notification taps carry { postId, handle } and open the post. Images are picked from rpc('photos.list') (media team) or pasted as a URL. Every image URL in Chirp (images, avatar), Yellow Pages (photo) and News (cover) must be https on a Config.Media.allowedHosts host (no IP grabbers in the public feed). Search input longer than 40 bytes is cut, not rejected.

Yellow Pages (yp.*, accent #e1b52f) ​

RPCparams → result
yp.list{ category?, q?, mine? } → { listings, fee, expireHours, categories }
yp.create{ category, title, price?, description?, photo?, location? } → listing (charges ypPostFee)
yp.delete{ id } → true (own, or admin; no refund)
yp.businesses→ { job, label, icon, number, count, open }[] (on-duty players per Config.Social.businesses, cached 15 s)

Ads expire after ypExpireHours (filtered in queries, expired rows deleted at most every 5 min). Max active ads per player: ypMaxActivePerPlayer. Push: yp:new, yp:deleted. Call / Message open phone / messages with { number }.

Maps (maps.*) ​

RPCparams → result
maps.data→ { places, shared, pois, icons }
maps.savePlace{ name, icon, x, y, z?, street? } → place (max maxPlaces)
maps.updatePlace{ id, name?, icon? } → true
maps.deletePlace / maps.deleteShared{ id } → true
maps.share{ number, label, x, y, z? } → true (notifies + pushes maps:shared to the receiver; tapping the notification { sharedId } opens the Shared tab with that location's actions)
maps.distances (client){ points: {x,y}[] } → { me: {x,y,z,street,zone}, distances: number[] }

Waypoints use the core client RPC gps.setWaypoint. The map header is a stylised SVG (no game tiles): pins are projected into the bounds x −4000..4500, y −4000..8000 (web/src/apps/maps/geo.ts, reused by Transit).

Calendar (calendar.*, accent #e98a52) ​

Times are epoch milliseconds. Reminders: one server thread checks every calendarReminderCheck seconds (single indexed query, skipped when nobody is online) and notifies the owner and accepted invitees.

RPCparams → result
calendar.list{ from, to } → events (own + accepted invites; own events include invitees)
calendar.create{ title, startsAt, endsAt?, allDay?, location?, notes?, color?, reminder? (minutes), invite?: numbers[] } → event
calendar.update{ id, …partial, invite? } → event (owner; false clears optional fields)
calendar.delete{ id } → true (owner deletes; an invitee leaves)
calendar.invites→ pending invites { id, status, event, from }
calendar.respond{ id, accept } → true
calendar.next→ { title, startsAt, endsAt, time, date, location, allDay } | null (home / lock widget, next 7 days)

Push: calendar:update { id }, calendar:invite { id, eventId }. Notification taps: { inviteId } opens Invitations, { eventId, startsAt? } (reminders, accepted / declined invites) selects the day and opens the event once loaded. Server API: Phone.Calendar.add(identifier, { title, startsAt, endsAt?, location?, notes?, allDay?, color?, reminder? }) → eventId. Export: exports.np_phone:addCalendarEvent(serverIdOrIdentifier, event).

Stocks (stocks.*) ​

Prices follow a random walk with mild mean reversion towards base, ticking every stockTickMinutes on the server (latest price + stockHistory points persisted in phone_stocks). Crypto symbols allow 4 decimals.

RPCparams → result
stocks.list→ { stocks: { symbol, name, crypto, price, change, changePct, history }[], tickMinutes }
stocks.portfolio→ { holdings: { symbol, shares, cost, value, avgPrice }[], value, cost, bank }
stocks.buy{ symbol, shares } → { shares, cost, price, portfolio } (cost rounded up, charged from bank)
stocks.sell{ symbol, shares } → { shares, proceeds, price, portfolio } (rounded down, paid to bank)

Push: stocks:tick (summary for every symbol, broadcast).

Transit (transit.*, accent #da4d1e) ​

Lines come from Config.Social.transit.lines. Departures are computed in the NUI from the in-game clock: trains leave the first stop every headway in-game minutes between firstDeparture and lastDeparture and need travel minutes per stop. The nearest stop uses maps.distances.

RPCparams → result
transit.lines→ { lines, ticketPrice, ticketHours, tickets }
transit.buy→ { item = true } (when ticketItem is set and Phone.fw('addItem') succeeds) or { ticket } (digital)

News (news.*, App Store app) ​

Players with a job in Config.Social.newsJobs (or the admin ace) can publish and delete any article. Body format: # / ## headings, - lists, > quotes, **bold**, *italic*, blank-line paragraphs.

RPCparams → result
news.list{ cursor? } → { articles (with excerpt), cursor, canPublish }
news.get{ id } → article with body
news.publish{ title, body, cover? } → article
news.delete{ id } → true

Push: news:new, news:deleted (broadcast with Phone.pushAll). With newsNotifyAll = true every player with a loaded phone gets a "Breaking News" notification (Phone.notifyAll, data = { articleId } opens the article).

Browser dev ​

Every app has a mock.ts. Screenshot helpers (dev only, ignored in FiveM): ?app=chirp&view=compose|profile|search|activity|thread, ?app=yp&view=detail|post, ?app=calendar&view=new, ?app=stocks&view=detail, ?app=transit&view=line, ?app=news&view=reader.

np_* FiveM resources