Stories
Stories
Shared across every PremiumPress product. 8 classes in inc/Stories.
Cleanup Cleanup.php
Stories — expiry housekeeping. Expiry itself is NOT done here. A frame stops being public the moment its _ppt_story_expires timestamp passes, because every read in Story goes through a meta_query that compares against time() — so visibility is exact to the second and survives a missed cron entirely. That matters: Ppt\Directory\Expiry's sweep runs DAILY, and a story with a 24-hour life cannot wait up to a day to disappear. What this class does is the disposal pass afterwards: an expired frame stays in the owner's own hub archive for Story::ARCHIVE_DAYS so they can see what they posted, then it and its attachment are deleted for good. It also takes a listing's frames with the listing when one is deleted, so no orphan tray survives its profile.
Hub Hub.php
Stories — the member hub panel ("My stories"). Not a Service: it renders on demand from inc/Account/views/account.php, the way Ppt\Account\Collections::renderPanel() does, so there is nothing to hook. One card per profile the member owns, because a frame belongs to a listing and a member may run several. Each card shows what is live and what has expired, side by side — an expired frame is immutable (it can only be deleted, never revived), so it is marked rather than offered an action. That is the only place on the site where expired frames are visible at all; every public read drops them.
Page Page.php
Stories — the public gallery page at /stories/. The ring tray is a strip you drop into a design; this is the standalone page it points at — every profile with something running right now, as a grid of rings. Built the same way Ppt_vt\Explore builds the Shorts feed: own rewrite rule and query var, rendered inside get_header()/get_footer() so it adopts the active design's shell, with the frame data embedded so the viewer opens instantly. Registered only where the feature is (ThemeProfiles::supportsStories), so the route does not exist on the other 13 product lines.
Seen Seen.php
Stories — seen state. Which trays a viewer has already watched, so the ring can be drawn filled (something new) or hollow (all caught up), and the viewer can skip ahead to the first tray with something unseen. Two stores, because a tray is public and most viewers are not signed in: - a signed-in member: user meta, so it follows them between devices; - a guest: localStorage, written by assets/js/stories.js and never sent here. Stored as listingId => the unix time of the newest frame that viewer has watched. A timestamp, not a frame id, so a tray is "seen" as long as nothing NEWER has been posted — the marker survives the frame it referred to expiring and being purged.
Story Story.php
Stories — the model. An ephemeral frame a profile owner posts to their own listing: one image or one short clip that shows in a ring-avatar tray and stops being public TTL seconds later. ONE POST PER FRAME. A "story" in the Instagram sense — the set a viewer taps through — is not a record here; it is simply this listing's unexpired frames, oldest first. Modelling the set would need a grouping key that expires as a unit, which is wrong: a frame posted at 09:00 and one posted at 23:00 must drop out of the tray at different times. Per-frame expiry falls straight out of the meta this way, and the viewer just plays whatever is still visible. Frames hang off the LISTING, not the member — a member may own several profiles on an escort or dating site and each is a separate identity with its own tray. That mirrors how Ppt_es\LiveChannel treats one profile as one channel. Shared, non-fork code: dating has no fork of its own (it runs on Ppt\Directory*) and escort does, so the engine cannot live in either. Every service in this folder self-gates on ThemeProfiles::supportsStories(), which is false for the other 13 product lines, so the folder is inert in their builds.
Tray Tray.php
Stories — the ring strip, and the front-end assets for the whole feature. Two shapes come out of here: - strip() a horizontal row of ring avatars (the homepage block, an archive header); - ring() ONE ring, wrapped around a profile page's own photo. The ring is the whole affordance: a listing with nothing to show gets no ring at all, rather than a flat circle that does nothing when tapped. An owner looking at their own profile always gets the tile, because "add" is the point of it for them. Assets are enqueued across the front end (not per-template) for the same reason Ppt\Account\Collections does it: rings turn up on the homepage block, in a search header and on a single page, and the script no-ops when it finds nothing to attach to. Two small files, one gate.
Uploader Uploader.php
Stories — the posting endpoints. One file per request (the same XHR contract the listing editor's Uppy uploader uses), except that a story frame goes LIVE on upload: there is no draft state and no form to save afterwards, so the attachment is created and published as a frame in a single round trip. Deliberately NOT routed through a fork's Ppt*\Uploads: both Ppt\Directory\ and Ppt_es\ define that class and only one ships in a given build, so a shared caller would break the other. The mime allow-list and the wp_handle_upload call are therefore restated here, kept in step with inc/_es/Uploads.php. The listing editor's listings_img_uploads setting is intentionally NOT consulted: that governs the photos ON a listing, and Stories has its own gate (ThemeProfiles::supportsStories) plus per-listing ownership. Turning listing photos off should not silently disable a separate feature.
Viewer Viewer.php
Stories — the viewer. One full-screen overlay per page, printed empty in the footer and filled by assets/js/stories.js from the frame data a tray embedded. It is an overlay, NOT a route. A story is a few seconds of someone's day, and sending a visitor to a URL for it would cost a page load each way and put the tray behind the back button. Deep links still work — /?story=<listing> opens the overlay over whatever page received it, so a shared link lands somewhere real if the story has already expired.