feat(tiktok): photo carousel support + Content Sharing API UX compliance#25
Merged
Merged
Conversation
Adds the enum case TikTokPhoto = 'tiktok_photo' with maxMediaCount 35, supportsImage true, supportsVideo false, 1:1 aspect ratio. The publisher service already calls /post/publish/content/init/ correctly — this enum case is the missing data layer that lets validation, UI, and defaults treat photo posts as a first-class content type.
TikTok's photo endpoint caps the 'title' field at 90 UTF-16 runes but allows 'description' up to 4000. Today the publisher always sends the caption in 'title', which would silently fail for any photo caption longer than 90 chars. Split buildPostInfo into buildVideoPostInfo (uses title) and buildPhotoPostInfo (uses description, omits Duet/ Stitch/AIGC since they don't apply to photos).
## Photo carousel support - Adds `ContentType::TikTokPhoto` enum case (max 35 photos, 1:1 aspect, supportsImage true, supportsVideo false) and JS mirror in content-type.ts. - Variant pill picker (Video / Photo carousel) at the top of TikTokSettings, mirroring the Instagram pattern. Wired through ScheduleTab to the parent editor's existing update:platformContentType emit. - i18n keys for variant_label / variant.video / variant.photo in en/pt-BR/es. - Publisher: split buildPostInfo into buildVideoPostInfo (uses `title`, TikTok cap 2200 chars) and buildPhotoPostInfo (uses `description`, cap 4000 chars; omits Duet/Stitch/AIGC since they don't apply). Removed the no-longer-needed queryCreatorInfo() call from publishVideo/publishPhotos — its only previous consumer (silent privacy_level fallback) is gone. ## UX Content Sharing API compliance Per TikTok review feedback citing https://developers.tiktok.com/doc/content-sharing-guidelines#required_ux_implementation_in_your_app Point 1 — already satisfied (creator_info fetch + nickname display). Point 2/4 — Music Usage Confirmation declaration is now always visible in TikTokSettings; text changes between "Music Usage Confirmation" and "Branded Content Policy and Music Usage Confirmation" based on toggle state. Previously the entire `<p>` block was conditional on a brand toggle being selected, hiding the baseline declaration. Point 2b — privacy_level may not have a default. UI was already correct; backend hardened: UpdatePostRequest now requires meta.privacy_level for tiktok platforms when status is publishing/scheduled (via withValidator); TikTokPublisher::resolveRequiredPrivacyLevel throws TikTokPublishException (ContentPolicy category) when missing instead of silently falling back to the creator's preferred level. Point 2c — interaction settings now condition on content type: - Photo posts hide Duet/Stitch (they don't apply per TikTok docs). - Photo posts hide AIGC (also video-only). - Video posts hide Auto Add Music (photos-only feature). - Max-duration warning hidden when not a video post. Source of truth is the user-selected contentType prop, not inferred from media — ensures the UI reacts immediately to the variant pill. Point 3a — publish button stays disabled when Disclose toggle is on without a sub-selection (already the case via tiktokComplianceValid). The disabled tooltip now uses the verbatim TikTok-required text "You need to indicate if your content promotes yourself, a third party, or both." instead of the generic "Some platform settings are incomplete..." when the only blocker is TikTok disclosure incompleteness. Point 3b — SELF_ONLY (Only me) privacy option is no longer filtered out when Branded Content is checked. It is rendered disabled with a hover tooltip "Branded content visibility cannot be set to private." plus a persistent amber warning paragraph below the dropdown. When the user toggles Branded Content while privacy is SELF_ONLY, the privacy clears and a vue-sonner warning toast surfaces the change. ## Cross-cutting - New `resources/js/enums/platform.ts` mirrors the PHP Platform enum, used in Edit.vue (tiktokComplianceValid + tiktokDisclosureIncomplete) and ScheduleTab.vue (all selected*Platforms computeds) to replace string literal comparisons against `'tiktok'` / `'facebook'` / etc. - PostPlatformFactory tiktok() state defaults meta.privacy_level to SELF_ONLY so existing test fixtures keep passing under the new publisher/FormRequest requirements. ## Tests - New tests/Unit/Enums/PostPlatform/TikTokPhotoContentTypeTest.php covering the new enum case (4 tests). - TikTokPublisherTest: added "video uses title not description" and "throws when meta.privacy_level missing" regression tests; renamed two existing tests that depended on the removed silent fallback. - New tests/Feature/UpdatePostRequestTest.php with 3 tests covering the FormRequest's privacy_level enforcement (publish-rejected, publish-passes, draft-allowed). Full Pest suite: 1490 passed, 2 skipped (pre-existing).
… error
- ContentType::description() now reads from posts.content_types.{value}.description
instead of hardcoded English. Adds the missing tiktok_photo entries in en/pt-BR/es
and syncs three Pinterest descriptions that had drifted between the i18n file and
the previously-hardcoded enum strings (the enum strings were the user-visible source).
- UpdatePostRequest::withValidator surfaces the privacy_required validation error
via posts.form.tiktok.privacy_required (added in en/pt-BR/es) instead of an
English string.
- Drops a verbose 3-line comment in withValidator that explained obvious code.
Removes the messages() override on UpdatePostRequest. The 4 messages it
defined were hardcoded English strings that bypassed Laravel's built-in
validation translations. With the override gone, errors flow through
lang/{locale}/validation.php which is already translated for en/pt-BR/es.
Trade-off: default messages reference the field path (e.g.
'platforms.0.content_type'). Acceptable here because the editor renders
errors inline next to each field rather than as a flat list.
The TikTok platform tile was getting silently disabled when the user attached images before selecting TikTok: a fresh post defaults TikTok to tiktok_video, the compliance check rejects images for video posts, and platformIssues marks the tile as un-clickable. Watch the media ref. If the first attached item is an image and the TikTok post-platform's content_type is still tiktok_video (or vice versa for video media on tiktok_photo), flip the content_type to match. The variant picker inside TikTokSettings can still override afterwards; this only covers the gap between adding media and opening the panel. Reproducer: new post → attach image → click TikTok tile → previously disabled with 'Only videos are allowed for this format', now enabled and content_type lands on tiktok_photo.
…viable variant Reverts the previous reactive watcher approach. Replaces it with two small, declarative changes: 1. platformIssues for TikTok now checks whether ANY variant (tiktok_video / tiktok_photo) accepts the current media. The tile only stays blocked when neither variant fits (e.g. a GIF, mixed media). This matches the platform's real capability — TikTok does accept both videos and photos — and stops disabling the tile based on the post-platform's stale default content_type. 2. togglePlatform, when SELECTING TikTok, switches the post-platform's content_type to the variant that matches the first attached media (tiktok_video for video, tiktok_photo for image). Runs only on selection, not as a watcher — no implicit reactivity, no side effects on subsequent media changes. The variant picker inside TikTokSettings remains the user's manual override. Reproducer fixed: new post → attach image → TikTok tile is now clickable instead of greyed out with 'Only videos are allowed for this format'. After clicking, content_type is tiktok_photo.
…with variant pickers Replaces two TikTok-specific code paths (one in platformIssues, one in togglePlatform) with a single generic helper. PLATFORM_VARIANTS maps each platform that exposes multiple content types via a variant picker to the list of those content types; firstCompatibleVariant returns the first variant that fits the current media (or null when none fits). Both consumers — the tile-level compliance check and the on-select content_type snap — now share the same helper. Adding another platform later (Instagram already has multiple variants if its default ever changes) only requires extending the map, no logic changes. Net effect: -15 lines, no TikTok-specific branching in logic, only data.
…match as TikTok Pinterest has the same UX bug: default content_type is pinterest_pin (image-only, 1 file). If the user attaches a video, the tile would silently disable; if they attach 2+ images, also disabled — even though pinterest_video_pin and pinterest_carousel exist as alternatives. List all three Pinterest variants so the tile's lenient compliance check considers each, and togglePlatform snaps to whichever fits the attached media on selection. Instagram, Facebook, and LinkedIn intentionally stay out: their defaults (feed/post) accept both images and videos, so there's no silent block. Including them would mask real per-variant errors (e.g. Reel + image incompatibility) — that's a user-visible mistake we want surfaced, not hidden at the tile level. The map is documented to that effect.
Two related fixes that together eliminate the 'Loading your TikTok account settings…' flicker users were seeing on every keystroke / variant click in the post editor: 1. PostController::edit no longer wraps tiktokCreatorInfos in Inertia::defer. The map is computed during the initial render and shipped as a regular prop. Without defer, the prop never resets to null between Inertia visits, so the loading line never reappears. 2. TikTokCreatorInfo::fetch is now wrapped in a 5-minute Cache::remember keyed by social_account_id. Autosaves (which round-trip through PostController::update → back() → edit() again) used to issue a fresh TikTok API call for every connected account on every save — now the cache short-circuits them. Creator info changes very rarely (only when the user updates privacy settings on TikTok itself), so five minutes of staleness is acceptable; the worst case is a slightly out-of-date privacy-options list that corrects on next page load. Frontend cleanup: dropped the creatorInfoLoading prop, the inline loading <p>, and the now-orphaned posts.form.tiktok.creator_info_loading i18n key in en/pt-BR/es. ScheduleTab no longer passes the prop.
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.
Summary
ContentType::TikTokPhoto, variant picker UI, publisher fix to use thedescriptionfield which has a 4000-char cap vs the 90-chartitlecap)resources/js/enums/platform.tsmirrors the PHPPlatformenum; Edit.vue and ScheduleTab.vue now use the enum instead of string literalsprivacy_level; publisher throws instead of silently defaultingTikTok UX Guideline checklist
Test plan
meta.privacy_level; verify 422 withplatforms.0.meta.privacy_levelerror; draft saves still work without itFiles changed
13 files modified + 3 new files:
Backend (PHP):
ContentType.php,TikTokPublisher.php,UpdatePostRequest.php,PostPlatformFactory.phpFrontend (Vue/TS):
TikTokSettings.vue,ScheduleTab.vue,Edit.vue,content-type.ts, newplatform.tsi18n:
lang/{en,pt-BR,es}/posts.phpTests: new
TikTokPhotoContentTypeTest.php, newUpdatePostRequestTest.php, modifiedTikTokPublisherTest.phpResubmission to TikTok
Recording a screencast walking through the interactions in order is the next step before resubmitting to TikTok review: