diff --git a/app/Http/Controllers/Auth/InstagramController.php b/app/Http/Controllers/Auth/InstagramController.php index 5a6fdbea7..dfc00e71e 100644 --- a/app/Http/Controllers/Auth/InstagramController.php +++ b/app/Http/Controllers/Auth/InstagramController.php @@ -25,6 +25,8 @@ class InstagramController extends SocialController 'instagram_business_basic', 'instagram_business_content_publish', 'instagram_business_manage_insights', + // first comment after publish (FirstCommentPoster) + 'instagram_business_manage_comments', ]; public function connect(Request $request): Response diff --git a/app/Http/Controllers/Auth/InstagramFacebookController.php b/app/Http/Controllers/Auth/InstagramFacebookController.php index 6f2a2af94..273428054 100644 --- a/app/Http/Controllers/Auth/InstagramFacebookController.php +++ b/app/Http/Controllers/Auth/InstagramFacebookController.php @@ -49,6 +49,8 @@ class InstagramFacebookController extends MetaController 'instagram_basic', 'instagram_content_publish', 'instagram_manage_insights', + // first comment after publish (FirstCommentPoster) + 'instagram_manage_comments', ]; public function connect(Request $request): Response diff --git a/app/Jobs/PublishToSocialPlatform.php b/app/Jobs/PublishToSocialPlatform.php index 096463a89..066ec7fbf 100644 --- a/app/Jobs/PublishToSocialPlatform.php +++ b/app/Jobs/PublishToSocialPlatform.php @@ -29,6 +29,7 @@ use App\Services\Social\Telegram\TelegramPublisher; use App\Services\Social\ThreadsPublisher; use App\Services\Social\TikTokPublisher; +use App\Services\Social\FirstCommentPoster; use App\Services\Social\XPublisher; use App\Services\Social\YouTubePublisher; use App\Support\Social\GoogleBusinessDerivativeCleaner; @@ -126,6 +127,15 @@ public function handle(): void $result = $publisher->publish($this->postPlatform); $this->recordPublishResult($result); + + // The post is live; a failed first comment must never fail it — + // FirstCommentPoster logs and swallows its own errors. Posts + // held in pending review (Google Business) get no comment. + if ($this->postPlatform->status === PostPlatformStatus::Published + && ($externalId = (string) data_get($result, 'id')) !== '') { + app(FirstCommentPoster::class)->post($this->postPlatform, $externalId); + } + break; } catch (PlatformUnavailableException $e) { $this->rescheduleForRetry($e); diff --git a/app/Mcp/Tools/Post/CreatePostTool.php b/app/Mcp/Tools/Post/CreatePostTool.php index 0183b2663..a446f3b8c 100644 --- a/app/Mcp/Tools/Post/CreatePostTool.php +++ b/app/Mcp/Tools/Post/CreatePostTool.php @@ -79,7 +79,7 @@ public function schema(JsonSchema $schema): array ->items($schema->object(fn ($p) => [ 'social_account_id' => $p->string()->required()->description('UUID of the connected social account.'), 'content_type' => $p->string()->required()->description('Format for this platform (e.g. linkedin_post, x_post, instagram_feed).'), - 'meta' => $p->object()->description('Per-platform metadata. Instagram/Facebook: aspect_ratio (1:1|4:5|16:9|original). TikTok: privacy_level PUBLIC_TO_EVERYONE|MUTUAL_FOLLOW_FRIENDS|FOLLOWER_OF_CREATOR|SELF_ONLY (required to publish) + flags (allow_comments, allow_duet, allow_stitch, disclose, brand_content_toggle, brand_organic_toggle, is_aigc, auto_add_music). SELF_ONLY cannot be combined with brand_content_toggle. Pinterest: board_id (required to publish — call ListPinterestBoardsTool first), title (≤100), link (destination URL). Pin description comes from the post content. Discord: channel_id (required to publish — call ListDiscordChannelsTool first), mentions ([{token,label}]), embeds ([{title,description,url,image,color}]). YouTube Shorts: description (optional plain text, at most 5000 bytes). Post content remains the title source, limited to 100 characters. Omit description or pass null to use the post content as description.'), + 'meta' => $p->object()->description('Per-platform metadata. Instagram/Facebook: aspect_ratio (1:1|4:5|16:9|original). TikTok: privacy_level PUBLIC_TO_EVERYONE|MUTUAL_FOLLOW_FRIENDS|FOLLOWER_OF_CREATOR|SELF_ONLY (required to publish) + flags (allow_comments, allow_duet, allow_stitch, disclose, brand_content_toggle, brand_organic_toggle, is_aigc, auto_add_music). SELF_ONLY cannot be combined with brand_content_toggle. Pinterest: board_id (required to publish — call ListPinterestBoardsTool first), title (≤100), link (destination URL). Pin description comes from the post content. Discord: channel_id (required to publish — call ListDiscordChannelsTool first), mentions ([{token,label}]), embeds ([{title,description,url,image,color}]). YouTube Shorts: description (optional plain text, at most 5000 bytes). Post content remains the title source, limited to 100 characters. Omit description or pass null to use the post content as description. YouTube/Instagram: first_comment (≤2200, posted by the account right after publish).'), ])) ->description('Platforms to publish on. Accounts not listed remain available but disabled.'), ]; diff --git a/app/Mcp/Tools/Post/UpdatePostTool.php b/app/Mcp/Tools/Post/UpdatePostTool.php index 0b2af698e..a59ebd888 100644 --- a/app/Mcp/Tools/Post/UpdatePostTool.php +++ b/app/Mcp/Tools/Post/UpdatePostTool.php @@ -119,7 +119,7 @@ public function schema(JsonSchema $schema): array ->items($schema->object(fn ($p) => [ 'id' => $p->string()->required()->description('UUID of the post_platform row (from get-post-tool / list-posts-tool).'), 'content_type' => $p->string()->description('New content_type for this platform.'), - 'meta' => $p->object()->description('Per-platform metadata override. Instagram/Facebook: aspect_ratio. TikTok: privacy_level PUBLIC_TO_EVERYONE|MUTUAL_FOLLOW_FRIENDS|FOLLOWER_OF_CREATOR|SELF_ONLY (required to publish) + flags. SELF_ONLY cannot be combined with brand_content_toggle. Pinterest: board_id (required to publish — call ListPinterestBoardsTool first), title (≤100), link (destination URL). Pin description comes from the post content. Discord: channel_id (required to publish — call ListDiscordChannelsTool first), mentions, embeds. Merged with existing meta. YouTube Shorts: description (optional plain text, at most 5000 bytes). Post content remains the title source, limited to 100 characters. Omit description to keep the current override; pass null to remove it and use the post content as description.'), + 'meta' => $p->object()->description('Per-platform metadata override. Instagram/Facebook: aspect_ratio. TikTok: privacy_level PUBLIC_TO_EVERYONE|MUTUAL_FOLLOW_FRIENDS|FOLLOWER_OF_CREATOR|SELF_ONLY (required to publish) + flags. SELF_ONLY cannot be combined with brand_content_toggle. Pinterest: board_id (required to publish — call ListPinterestBoardsTool first), title (≤100), link (destination URL). Pin description comes from the post content. Discord: channel_id (required to publish — call ListDiscordChannelsTool first), mentions, embeds. Merged with existing meta. YouTube Shorts: description (optional plain text, at most 5000 bytes). Post content remains the title source, limited to 100 characters. Omit description to keep the current override; pass null to remove it and use the post content as description. YouTube/Instagram: first_comment (≤2200, posted by the account right after publish).'), ])) ->description('Platforms to enable for publishing. Any platform NOT listed will be disabled. Pass an empty array to disable all.'), ]; diff --git a/app/Services/Social/FirstCommentPoster.php b/app/Services/Social/FirstCommentPoster.php new file mode 100644 index 000000000..b0a7b32a4 --- /dev/null +++ b/app/Services/Social/FirstCommentPoster.php @@ -0,0 +1,102 @@ +meta, 'first_comment')); + + if ($comment === '') { + return; + } + + try { + match ($postPlatform->platform) { + Platform::YouTube => $this->postYouTubeComment($postPlatform, $externalId, $comment), + Platform::Instagram, Platform::InstagramFacebook => $this->postInstagramComment($postPlatform, $externalId, $comment), + default => Log::warning('First comment is not supported for this platform', [ + 'platform' => $postPlatform->platform->value, + 'post_platform_id' => $postPlatform->id, + ]), + }; + } catch (\Throwable $e) { + Log::warning('First comment failed (post itself is published)', [ + 'platform' => $postPlatform->platform->value, + 'post_platform_id' => $postPlatform->id, + 'error' => $e->getMessage(), + ]); + } + } + + /** + * commentThreads.insert needs the youtube.force-ssl scope — requested at + * connect time since the platform was added, so every account has it. + */ + private function postYouTubeComment(PostPlatform $postPlatform, string $videoId, string $comment): void + { + $account = $postPlatform->socialAccount; + $api = rtrim((string) config('trypost.platforms.youtube.data_api'), '/'); + + $response = $this->socialHttp() + ->withToken($account->access_token) + ->post("{$api}/commentThreads?part=snippet", [ + 'snippet' => [ + 'videoId' => $videoId, + 'topLevelComment' => [ + 'snippet' => ['textOriginal' => $comment], + ], + ], + ]); + + if ($response->failed()) { + Log::warning('YouTube first comment failed', [ + 'post_platform_id' => $postPlatform->id, + 'status' => $response->status(), + 'body' => $this->redactResponseBody($response->body()), + ]); + } + } + + /** + * Needs instagram_business_manage_comments (direct) / instagram_manage_comments + * (via Facebook Page) — accounts connected before the scope was requested + * get a 4xx here, which is logged and ignored; reconnecting fixes it. + */ + private function postInstagramComment(PostPlatform $postPlatform, string $mediaId, string $comment): void + { + $account = $postPlatform->socialAccount; + $baseUrl = $account->platform->instagramGraphBaseUrl(); + + $response = $this->socialHttp()->post("{$baseUrl}/{$mediaId}/comments", [ + 'message' => $comment, + 'access_token' => $account->access_token, + ]); + + if ($response->failed()) { + Log::warning('Instagram first comment failed', [ + 'post_platform_id' => $postPlatform->id, + 'status' => $response->status(), + 'body' => $this->redactResponseBody($response->body()), + ]); + } + } +} diff --git a/app/Support/PostPlatformMetaRules.php b/app/Support/PostPlatformMetaRules.php index f7608db84..403e9c8f9 100644 --- a/app/Support/PostPlatformMetaRules.php +++ b/app/Support/PostPlatformMetaRules.php @@ -53,6 +53,11 @@ public static function rules(): array 'platforms.*.meta.brand_content_toggle' => ['sometimes', 'boolean'], 'platforms.*.meta.brand_organic_toggle' => ['sometimes', 'boolean'], + // First comment posted right after a successful publish (YouTube and + // Instagram). Capped at Instagram's comment limit, the stricter of + // the two. + 'platforms.*.meta.first_comment' => ['sometimes', 'nullable', 'string', 'max:2200'], + // Pinterest 'platforms.*.meta.board_id' => ['sometimes', 'nullable', 'string'], 'platforms.*.meta.title' => ['sometimes', 'nullable', 'string', 'max:100'], @@ -105,6 +110,7 @@ public static function messages(): array 'platforms.*.meta.title.max' => __('posts.form.pinterest.title_max'), 'platforms.*.meta.event.end_date.after_or_equal' => __('posts.form.google_business.event_end_date_before_start'), 'platforms.*.meta.event.title.max' => __('posts.form.google_business.title_max'), + 'platforms.*.meta.first_comment.max' => __('posts.form.first_comment.max'), ]; } @@ -121,6 +127,7 @@ public static function attributes(): array 'platforms.*.meta.link' => __('posts.form.pinterest.link'), 'platforms.*.meta.event.title' => __('posts.form.google_business.event_title'), 'platforms.*.meta.call_to_action.url' => __('posts.form.google_business.cta_url'), + 'platforms.*.meta.first_comment' => __('posts.form.first_comment.label'), ]; } diff --git a/lang/ar/posts.php b/lang/ar/posts.php index 1005d7b03..d9c4c1d81 100644 --- a/lang/ar/posts.php +++ b/lang/ar/posts.php @@ -151,6 +151,13 @@ 'description_invalid' => 'يجب أن يكون وصف YouTube نصًا صالحًا.', 'description_bytes' => ':used / :limit بايت', ], + 'first_comment' => [ + 'label' => 'التعليق الأول', + 'placeholder' => 'رابط وإضافات للتعليق الأول…', + 'hint' => 'يُنشر باسم قناتك فور نشر الفيديو.', + 'hint_instagram' => 'يُنشر فور النشر — المكان الكلاسيكي للروابط التي لا تكون قابلة للنقر في وصف Instagram. أعد ربط الحساب مرة واحدة لمنح إذن التعليقات.', + 'max' => 'لا يمكن أن يتجاوز التعليق الأول 2200 حرف.', + ], 'pinterest' => [ 'settings' => 'إعدادات Pinterest', 'posting_to' => 'النشر إلى', diff --git a/lang/de/posts.php b/lang/de/posts.php index 0bd362f66..ca3cf975f 100644 --- a/lang/de/posts.php +++ b/lang/de/posts.php @@ -153,6 +153,13 @@ 'description_invalid' => 'Die YouTube-Beschreibung muss gültiger Text sein.', 'description_bytes' => ':used / :limit Bytes', ], + 'first_comment' => [ + 'label' => 'Erster Kommentar', + 'placeholder' => 'Link und Extras für den ersten Kommentar…', + 'hint' => 'Wird direkt nach der Veröffentlichung vom Kanal gepostet.', + 'hint_instagram' => 'Wird direkt nach dem Beitrag gepostet — der klassische Ort für Links, die in Instagram-Beschreibungen nicht klickbar sind. Konto einmal neu verbinden, um die Kommentar-Berechtigung zu erteilen.', + 'max' => 'Der erste Kommentar darf 2200 Zeichen nicht überschreiten.', + ], 'pinterest' => [ 'settings' => 'Pinterest-Einstellungen', 'posting_to' => 'Veröffentlichen auf', diff --git a/lang/el/posts.php b/lang/el/posts.php index 6d8516056..3b4517ad8 100644 --- a/lang/el/posts.php +++ b/lang/el/posts.php @@ -151,6 +151,13 @@ 'description_invalid' => 'Η περιγραφή YouTube πρέπει να είναι έγκυρο κείμενο.', 'description_bytes' => ':used / :limit byte', ], + 'first_comment' => [ + 'label' => 'Πρώτο σχόλιο', + 'placeholder' => 'Σύνδεσμος και πρόσθετα για το πρώτο σχόλιο…', + 'hint' => 'Δημοσιεύεται από το κανάλι σας αμέσως μετά τη δημοσίευση του βίντεο.', + 'hint_instagram' => 'Δημοσιεύεται αμέσως μετά την ανάρτηση — το κλασικό σημείο για συνδέσμους που οι λεζάντες του Instagram δεν κάνουν κλικαρίσιμους. Επανασυνδέστε τον λογαριασμό μία φορά για να δώσετε το δικαίωμα σχολίων.', + 'max' => 'Το πρώτο σχόλιο δεν μπορεί να υπερβαίνει τους 2200 χαρακτήρες.', + ], 'pinterest' => [ 'settings' => 'Ρυθμίσεις Pinterest', 'posting_to' => 'Δημοσίευση σε', diff --git a/lang/en/posts.php b/lang/en/posts.php index c97d9ca3e..8c45c302d 100644 --- a/lang/en/posts.php +++ b/lang/en/posts.php @@ -151,6 +151,13 @@ 'description_invalid' => 'The YouTube description must be valid text.', 'description_bytes' => ':used / :limit bytes', ], + 'first_comment' => [ + 'label' => 'First comment', + 'placeholder' => 'Link and extras for the first comment…', + 'hint' => 'Posted by your channel right after the video is published.', + 'hint_instagram' => 'Posted right after publishing — the classic spot for links Instagram captions can\'t make clickable. Requires reconnecting the account once to grant the comments permission.', + 'max' => 'First comment may not exceed 2200 characters.', + ], 'pinterest' => [ 'settings' => 'Pinterest Settings', 'posting_to' => 'Posting to', diff --git a/lang/es/posts.php b/lang/es/posts.php index b0f6ffd60..e9071b5c3 100644 --- a/lang/es/posts.php +++ b/lang/es/posts.php @@ -151,6 +151,13 @@ 'description_invalid' => 'La descripción de YouTube debe ser texto válido.', 'description_bytes' => ':used / :limit bytes', ], + 'first_comment' => [ + 'label' => 'Primer comentario', + 'placeholder' => 'Enlace y extras para el primer comentario…', + 'hint' => 'Se publica desde tu canal justo después de publicar el vídeo.', + 'hint_instagram' => 'Se publica justo después de la publicación: el lugar clásico para enlaces que Instagram no hace clicables en la descripción. Reconecta la cuenta una vez para conceder el permiso de comentarios.', + 'max' => 'El primer comentario no puede superar los 2200 caracteres.', + ], 'pinterest' => [ 'settings' => 'Configuración de Pinterest', 'posting_to' => 'Publicando en', diff --git a/lang/fr/posts.php b/lang/fr/posts.php index 4cb63393d..393221b1b 100644 --- a/lang/fr/posts.php +++ b/lang/fr/posts.php @@ -151,6 +151,13 @@ 'description_invalid' => 'La description YouTube doit être un texte valide.', 'description_bytes' => ':used / :limit octets', ], + 'first_comment' => [ + 'label' => 'Premier commentaire', + 'placeholder' => 'Lien et compléments pour le premier commentaire…', + 'hint' => 'Publié par votre chaîne juste après la mise en ligne de la vidéo.', + 'hint_instagram' => 'Publié juste après la publication — l\'endroit classique pour les liens que les légendes Instagram ne rendent pas cliquables. Reconnectez le compte une fois pour accorder l\'autorisation de commenter.', + 'max' => 'Le premier commentaire ne peut pas dépasser 2200 caractères.', + ], 'pinterest' => [ 'settings' => 'Paramètres Pinterest', 'posting_to' => 'Publier sur', diff --git a/lang/it/posts.php b/lang/it/posts.php index 3f3aa3316..22d0f8104 100644 --- a/lang/it/posts.php +++ b/lang/it/posts.php @@ -151,6 +151,13 @@ 'description_invalid' => 'La descrizione YouTube deve essere testo valido.', 'description_bytes' => ':used / :limit byte', ], + 'first_comment' => [ + 'label' => 'Primo commento', + 'placeholder' => 'Link ed extra per il primo commento…', + 'hint' => 'Pubblicato dal tuo canale subito dopo la pubblicazione del video.', + 'hint_instagram' => 'Pubblicato subito dopo il post: il posto classico per i link che le didascalie di Instagram non rendono cliccabili. Ricollega l\'account una volta per concedere il permesso sui commenti.', + 'max' => 'Il primo commento non può superare i 2200 caratteri.', + ], 'pinterest' => [ 'settings' => 'Impostazioni Pinterest', 'posting_to' => 'Pubblicazione su', diff --git a/lang/ja/posts.php b/lang/ja/posts.php index c2a011a02..e70d9b576 100644 --- a/lang/ja/posts.php +++ b/lang/ja/posts.php @@ -151,6 +151,13 @@ 'description_invalid' => 'YouTubeの説明には有効なテキストを入力してください。', 'description_bytes' => ':used / :limit バイト', ], + 'first_comment' => [ + 'label' => '最初のコメント', + 'placeholder' => '最初のコメントに入れるリンクや補足…', + 'hint' => '動画公開直後にチャンネルから投稿されます。', + 'hint_instagram' => '投稿直後にコメントされます。Instagramのキャプションでクリックできないリンクの定番の置き場所です。コメント権限を付与するには一度アカウントを再連携してください。', + 'max' => '最初のコメントは2200文字を超えられません。', + ], 'pinterest' => [ 'settings' => 'Pinterest 設定', 'posting_to' => '投稿先', diff --git a/lang/ko/posts.php b/lang/ko/posts.php index bf42e246a..66a465721 100644 --- a/lang/ko/posts.php +++ b/lang/ko/posts.php @@ -151,6 +151,13 @@ 'description_invalid' => 'YouTube 설명은 유효한 텍스트여야 합니다.', 'description_bytes' => ':used / :limit 바이트', ], + 'first_comment' => [ + 'label' => '첫 댓글', + 'placeholder' => '첫 댓글에 넣을 링크와 추가 내용…', + 'hint' => '동영상 게시 직후 채널 명의로 작성됩니다.', + 'hint_instagram' => '게시 직후 작성됩니다 — Instagram 캡션에서 클릭되지 않는 링크를 넣는 고전적인 방법입니다. 댓글 권한을 부여하려면 계정을 한 번 다시 연결하세요.', + 'max' => '첫 댓글은 2200자를 초과할 수 없습니다.', + ], 'pinterest' => [ 'settings' => 'Pinterest 설정', 'posting_to' => '게시 대상', diff --git a/lang/nl/posts.php b/lang/nl/posts.php index da32a0986..5cf8bf9d3 100644 --- a/lang/nl/posts.php +++ b/lang/nl/posts.php @@ -151,6 +151,13 @@ 'description_invalid' => 'De YouTube-beschrijving moet geldige tekst zijn.', 'description_bytes' => ':used / :limit bytes', ], + 'first_comment' => [ + 'label' => 'Eerste reactie', + 'placeholder' => 'Link en extra\'s voor de eerste reactie…', + 'hint' => 'Direct na publicatie door je kanaal geplaatst.', + 'hint_instagram' => 'Direct na het bericht geplaatst — dé plek voor links die Instagram-bijschriften niet klikbaar maken. Verbind het account één keer opnieuw om de reactietoestemming te geven.', + 'max' => 'De eerste reactie mag niet langer zijn dan 2200 tekens.', + ], 'pinterest' => [ 'settings' => 'Pinterest-instellingen', 'posting_to' => 'Posten naar', diff --git a/lang/pl/posts.php b/lang/pl/posts.php index 3393ecca7..8aa171284 100644 --- a/lang/pl/posts.php +++ b/lang/pl/posts.php @@ -151,6 +151,13 @@ 'description_invalid' => 'Opis YouTube musi być poprawnym tekstem.', 'description_bytes' => ':used / :limit bajtów', ], + 'first_comment' => [ + 'label' => 'Pierwszy komentarz', + 'placeholder' => 'Link i dodatki do pierwszego komentarza…', + 'hint' => 'Publikowany przez Twój kanał zaraz po opublikowaniu filmu.', + 'hint_instagram' => 'Publikowany zaraz po poście — klasyczne miejsce na linki, których opisy na Instagramie nie czynią klikalnymi. Połącz konto ponownie, aby nadać uprawnienie do komentarzy.', + 'max' => 'Pierwszy komentarz nie może przekraczać 2200 znaków.', + ], 'pinterest' => [ 'settings' => 'Ustawienia Pinterest', 'posting_to' => 'Publikowanie na', diff --git a/lang/pt-BR/posts.php b/lang/pt-BR/posts.php index 5cb562e66..985ed5cc1 100644 --- a/lang/pt-BR/posts.php +++ b/lang/pt-BR/posts.php @@ -151,6 +151,13 @@ 'description_invalid' => 'A descrição do YouTube deve ser um texto válido.', 'description_bytes' => ':used / :limit bytes', ], + 'first_comment' => [ + 'label' => 'Primeiro comentário', + 'placeholder' => 'Link e extras para o primeiro comentário…', + 'hint' => 'Publicado pelo seu canal logo após o vídeo ir ao ar.', + 'hint_instagram' => 'Publicado logo após a postagem — o lugar clássico para links que as legendas do Instagram não tornam clicáveis. Reconecte a conta uma vez para conceder a permissão de comentários.', + 'max' => 'O primeiro comentário não pode exceder 2200 caracteres.', + ], 'pinterest' => [ 'settings' => 'Configurações do Pinterest', 'posting_to' => 'Publicando em', diff --git a/lang/ru/posts.php b/lang/ru/posts.php index b64e8164b..887602c9f 100644 --- a/lang/ru/posts.php +++ b/lang/ru/posts.php @@ -151,6 +151,13 @@ 'description_invalid' => 'Описание YouTube должно быть корректным текстом.', 'description_bytes' => ':used / :limit байт', ], + 'first_comment' => [ + 'label' => 'Первый комментарий', + 'placeholder' => 'Ссылка и дополнения для первого комментария…', + 'hint' => 'Публикуется от имени канала сразу после выхода ролика.', + 'hint_instagram' => 'Публикуется сразу после поста — классическое место для ссылок, которые в описании Instagram некликабельны. Требуется один раз переподключить аккаунт, чтобы выдать право на комментарии.', + 'max' => 'Первый комментарий не может быть длиннее 2200 знаков.', + ], 'pinterest' => [ 'settings' => 'Настройки Pinterest', 'posting_to' => 'Публикация в', diff --git a/lang/tr/posts.php b/lang/tr/posts.php index 52f68be2d..51b85c2a3 100644 --- a/lang/tr/posts.php +++ b/lang/tr/posts.php @@ -153,6 +153,13 @@ 'description_invalid' => 'YouTube açıklaması geçerli bir metin olmalıdır.', 'description_bytes' => ':used / :limit bayt', ], + 'first_comment' => [ + 'label' => 'İlk yorum', + 'placeholder' => 'İlk yorum için bağlantı ve ekler…', + 'hint' => 'Video yayınlandıktan hemen sonra kanalınız tarafından gönderilir.', + 'hint_instagram' => 'Gönderiden hemen sonra paylaşılır — Instagram açıklamalarında tıklanamayan bağlantılar için klasik yer. Yorum iznini vermek için hesabı bir kez yeniden bağlayın.', + 'max' => 'İlk yorum 2200 karakteri aşamaz.', + ], 'pinterest' => [ 'settings' => 'Pinterest Ayarları', 'posting_to' => 'Şuraya paylaşılıyor', diff --git a/lang/uk/posts.php b/lang/uk/posts.php index 293a047a7..3373bd3e8 100644 --- a/lang/uk/posts.php +++ b/lang/uk/posts.php @@ -151,6 +151,13 @@ 'description_invalid' => 'Опис YouTube має бути коректним текстом.', 'description_bytes' => ':used / :limit байтів', ], + 'first_comment' => [ + 'label' => 'Перший коментар', + 'placeholder' => 'Посилання та доповнення для першого коментаря…', + 'hint' => 'Публікується від імені каналу одразу після виходу відео.', + 'hint_instagram' => 'Публікується одразу після поста — класичне місце для посилань, які в описі Instagram неклікабельні. Потрібно один раз перепідключити акаунт, щоб надати право на коментарі.', + 'max' => 'Перший коментар не може перевищувати 2200 знаків.', + ], 'pinterest' => [ 'settings' => 'Налаштування Pinterest', 'posting_to' => 'Публікація в', diff --git a/lang/zh/posts.php b/lang/zh/posts.php index f42ea1bcb..320fca19f 100644 --- a/lang/zh/posts.php +++ b/lang/zh/posts.php @@ -151,6 +151,13 @@ 'description_invalid' => 'YouTube 描述必须是有效文本。', 'description_bytes' => ':used / :limit 字节', ], + 'first_comment' => [ + 'label' => '首条评论', + 'placeholder' => '首条评论中的链接和补充内容…', + 'hint' => '视频发布后立即以频道名义发表。', + 'hint_instagram' => '发布后立即评论——这是放置 Instagram 描述中无法点击的链接的经典位置。需重新连接一次账号以授予评论权限。', + 'max' => '首条评论不能超过 2200 个字符。', + ], 'pinterest' => [ 'settings' => 'Pinterest 设置', 'posting_to' => '发布到', diff --git a/resources/js/components/posts/editor/InstagramSettings.vue b/resources/js/components/posts/editor/InstagramSettings.vue index 8b9803985..daea5d790 100644 --- a/resources/js/components/posts/editor/InstagramSettings.vue +++ b/resources/js/components/posts/editor/InstagramSettings.vue @@ -4,6 +4,7 @@ import { computed, ref } from 'vue'; import MediaRulesWarning from '@/components/posts/editor/MediaRulesWarning.vue'; import { Avatar } from '@/components/ui/avatar'; +import { Textarea } from '@/components/ui/textarea'; import { getPlatformLogo } from '@/composables/usePlatformLogo'; import { AspectRatio, type AspectRatioValue } from '@/types/aspect-ratio'; import { ContentType } from '@/types/content-type'; @@ -65,6 +66,13 @@ const pickAspectRatio = (value: AspectRatioValue) => { if (props.disabled) return; emit('update:meta', { ...props.meta, aspect_ratio: value }); }; + +const FIRST_COMMENT_MAX = 2200; + +const firstComment = computed({ + get: () => (props.meta?.first_comment as string | undefined) || '', + set: (value: string) => emit('update:meta', { ...props.meta, first_comment: value || null }), +});