Commit graph

20 commits

Author SHA1 Message Date
Paulo Castellano
2e9e0f716b feat: capture signup UTMs/IP and add GitHub OAuth login
Persists marketing attribution and registration metadata for new users
across the three signup paths (email, Google, GitHub):

- 5 utm_* columns + registration_ip on the users table
- PreservesUtmParameters trait stores incoming utm_* query params on
  the register/redirect GET, retrieves them on the POST/callback —
  surviving the OAuth round-trip via session
- request()->ip() captured at the controller layer

Adds GitHub as a second OAuth provider:

- GitHubController mirroring the Google one (now renamed from
  SocialLoginController for symmetry)
- Settings → Authentication can connect/disconnect GitHub like Google
- Single SocialLogin.vue component replaces the per-provider buttons
  on Login/Register, rendering each enabled provider plus a single
  "or continue with" divider

UserFactory gains defaults for the new nullable columns so model
strict-mode access in tests doesn't trip.
2026-05-04 18:42:25 -03:00
Paulo Castellano
770266d36b refactor: centralize media size limits in config + drop Document type
Three things in one move:

1. Centralize per-type size caps in config/trypost.php under media.max_size_mb.
   The MediaType enum now reads from there:

       MediaType::Image->maxSizeInMb()    // 10  (env: MEDIA_IMAGE_MAX_SIZE_MB)
       MediaType::Video->maxSizeInMb()    // 1024 (env: MEDIA_VIDEO_MAX_SIZE_MB)

   Plus convenience helpers maxSizeInBytes() and maxSizeInKb() so callers
   don't have to multiply themselves. StoreAssetRequest now uses
   MediaType::Video->maxSizeInKb() in its 'max:' rule and mimes derived
   from MediaType::{Image,Video}->allowedMimeTypes(). storeChunked
   validation moved to the new StoreChunkedAssetRequest FormRequest
   (also reads from the enum). MediaAttacher uses
   MediaType::Video->maxSizeInBytes() as the streaming-abort threshold
   and enforces the per-type cap after MIME resolution.

2. Drop MediaType::Document. We never accepted PDFs anywhere — the
   StoreAssetRequest mimes list excluded them, the storeChunked
   extension regex excluded them, MediaAttacher excluded them. The only
   places that referenced Document were:
     - Platform::allowedMediaTypes for LinkedIn/LinkedInPage (declared
       but unreachable)
     - HasMedia::getMediaType fallback when MIME wasn't image/video/*
   Both now cleaned up. HasMedia::getMediaType throws
   InvalidArgumentException for unsupported MIMEs instead of silently
   returning a fake 'document' type. Platform::LinkedIn now matches
   every other social platform: [Image, Video].

3. Add MediaType::fromMime($mime): ?self — replaces the inline mime →
   type loop that MediaAttacher used to roll. Returns null for
   unsupported MIMEs (caller decides how to react).

Tests:
- MediaTypeTest rewritten for the new shape (no Document, config-driven
  sizes, fromMime + size-helper coverage).
- PlatformTest no longer asserts Document on LinkedIn.
- HasMediaTest replaces the 'detects document type' case with one that
  asserts the throw on unsupported MIMEs. The 'add media from path'
  test now uses real PNG bytes from the fixture.
- AssetControllerTest chunked tests use real PNG bytes and assert 422
  (FormRequest unprocessable) for malformed Content-Range headers,
  matching the new validation layer.
2026-05-04 14:54:47 -03:00
Paulo Castellano
5658ea8c8c feat: update Bluesky to use public AppView and remove Mastodon access token from public status requests 2026-05-03 22:16:19 -03:00
Paulo Castellano
f4fd8a2aab refactor: centralize social API base URLs into the configuration file to improve maintainability and environment flexibility 2026-05-02 13:49:20 -03:00
Paulo Castellano
3c3b170b21 feat: @mentions in comments, AI Action layer + MCP tools, settings tabs
Mentions in post comments
- @mention autocomplete (workspace members, current user excluded) with
  marker syntax @[uuid] persisted, display names rendered via CommentBody
  chips; live edit replaces markers with names and converts back on save.
- NotifyMentions action with workspace-scoped membership check, dedupes
  same user, only newly-added mentions on update.
- Email + in-app via SendNotification job, respecting per-user
  notification_preferences.mentioned_in_comment.
- Heartbeat-based presence (Cache, 60s TTL, 30s ping) so online recipients
  get only the in-app notification — no email noise.
- Real-time bell on workspace.{id}.user.{id} private channel
  (NotificationCreated event), scoped channel name avoids client-side
  filtering and lays out a convention for future workspace channels.
- Mailable localized via lang/{en,es,pt-BR}/mail.php; Maizzle source
  template for the email is committed and built into resources/views/mail.

AI generation refactor (Action layer + MCP)
- Extracted Actions/Ai/Generate{Image,Video} with QuotaExhaustedException
  so agent tools and MCP tools share a single domain entry point.
- Mcp/Tools/Ai/Generate{Image,Video}Tool registered in TryPostServer; both
  return MediaResource payloads.
- Orientation::imageApiSize maps non-OpenAI ratios to 1:1/2:3/3:2.
- config/ai.php is now the single source of truth driven by env, removing
  the trypost.ai shim. Default text/image providers flipped to OpenAI.

Settings/UX
- /settings/workspace split into shadcn Tabs (Workspace / Brand / Users)
  with three components.
- /assets and the in-editor MediaPicker open the ImagePreviewDialog
  lightbox on image click while preserving action button behaviour.
- Comments tab landed via ?tab=comments&comment=<id> from notification
  click (scroll-to + temporary highlight).
- Mention autocomplete popover flips above when near the viewport bottom.
- Real social platform PNGs replace Tabler brand glyphs in schedule
  pills and post list, with hover tooltip carrying display_name + handle.

Bug fixes
- AcceptInvite: controller now passes workspace + role payload that the
  Vue page expects; login/register CTAs preselect the invite email.
- WorkspaceInvite mailable: stopped referencing nonexistent
  $invite->workspace and $invite->role; column added to the migration,
  Invite model casts role to WorkspaceRole, CreateInvite persists it.
- PostCommentCreated: added broadcastAs so .PostCommentCreated actually
  matches the Echo listener; payload now includes mentioned_users so
  receivers render the chip correctly without a refetch.
- Preview components for X/Pinterest/Threads/Bluesky/LinkedIn/Mastodon/
  TikTok/YouTube switched from item.type === 'image' to
  !isVideoMedia(item) so media without a persisted type still renders.
- UpdatePostRequest now accepts media.*.{type,mime_type,size,...} so the
  posts.media JSON keeps the metadata that the previews need.
- Removed throttle:6,1 from social connect routes (was 429ing legitimate
  OAuth retries).
- Used MediaType enum cases instead of literal 'image'/'video' strings
  when creating media rows.

Tests
- MentionParser unit tests, NotifyMentions feature tests including
  online/offline channel selection and preference gating, MCP AI tool
  happy paths, MentionedInComment mailable rendering, AcceptInvite +
  search-members + index mentioned_users path. 1229 passing.
2026-05-01 20:59:03 -03:00
Paulo Castellano
b2bf5c2059 feat: complete AI assistant with custom services and brand config
Baseline snapshot of custom AI implementation before Laravel AI SDK migration.

Includes:
- Custom services: GeminiTextGenerationService, TextGenerationService (OpenAI), ImageGenerationService, AudioGenerationService, VideoGenerationService
- IntentDetector for content moderation via keyword matching
- AI enums: Intent, Orientation, UsageType
- Blade prompt templates: system.blade.php, image.blade.php, video.blade.php
- AiMessage with content_html accessor (markdown rendering)
- AiUsageLog for monthly quota tracking per account
- PostAssistantController with regex-based [GENERATE_*] parsing
- WritingAssistantTab with markdown rendering, add-to-post, attachments
- Workspace brand fields (name, description, tone, voice_notes) in system prompt
- Session state block injected into prompts (thread counts, quota remaining)
- AttachmentCollector pattern will replace the regex approach in Phase 2
- Post comments with replies, emoji reactions, real-time via Echo
- Assets page with Unsplash + Giphy integrations
2026-04-16 09:25:08 -03:00
Paulo Castellano
80c193f69f feat: add localization files and update Instagram-Facebook environment variable key 2026-04-14 14:49:38 -03:00
Paulo Castellano
1a7af9728f Merge remote-tracking branch 'origin/main' into feature/google-auth-toggle 2026-04-14 14:29:56 -03:00
Paulo Castellano
d94374c31a refactor: remove TRYPOST_ prefix from platform env variables
Standardizes env naming to match existing convention (THREADS_CLIENT_ID,
GOOGLE_AUTH_ENABLED, etc).
2026-04-14 14:19:51 -03:00
Paulo Castellano
25c43a3906 refactor: rename TRYPOST_GOOGLE_AUTH_ENABLED to GOOGLE_AUTH_ENABLED
Aligns with existing Google env naming convention (GOOGLE_AUTH_CALLBACK).
2026-04-14 14:16:34 -03:00
Paulo Castellano
cb91529964 chore: working 2026-04-02 17:57:06 -03:00
Paulo Castellano
57df5d5253 feat: add TRYPOST_GOOGLE_AUTH_ENABLED toggle to show/hide Google login on auth pages 2026-04-01 15:27:26 -03:00
Paulo Castellano
56b8c92e72 refactor: settings redesign, Spanish translations, language system, strict_types
Settings pages:
- Redesign layout to match Sendkit (max-w-4xl, space-y-12, Separator sections)
- Merge Members page into Workspace settings with Table, invite Dialog, ConfirmDeleteModal
- Add workspace logo upload/delete routes and controller methods
- Translate all hardcoded strings in Workspace.vue modals

Language system:
- Drop languages table, replace language_id FK with locale string column on users
- Create config/languages.php for available languages and default locale
- Add Spanish (es) translations (13 files)
- Simplify HandleInertiaRequests, ProfileController, RegisteredUserController

Code quality:
- Add declare(strict_types=1) to all PHP files
- Fix MastodonPublisher using wrong attribute (filename -> original_filename)
- Fix HasMediaTest for new has_photo/photo_url accessors
- Fix PublishToSocialPlatformTest type error revealed by strict_types
- Remove orphaned Language model from AppServiceProvider morph map
- Update User TypeScript interface (has_photo, photo_url, locale)
- Eager load media relation on workspaces to prevent N+1
- Add 8 new tests for workspace logo upload/delete
- Update workspace settings test to assert members/invitations props

All 710 tests passing.
2026-03-30 00:20:43 -03:00
Paulo Castellano
a6517f4b5b feat: Introduce a self-hosted mode, refactor email notifications to use Mailable classes with new templates, and update the project README. 2026-01-18 17:48:12 -03:00
Paulo Castellano
52650909a4 feat: Add comprehensive support for new social media platforms, including publisher and controller tests, configuration, and factory updates. 2026-01-18 15:56:51 -03:00
Paulo Castellano
099cd5d118 feat: Add Mastodon social media integration
- Add Mastodon to Platform enum with color #6364FF, 500 char limit, 4 max images
- Add MastodonPost to ContentType enum
- Create MastodonController with dynamic OAuth app registration per instance
- Create MastodonPublisher service for posting statuses with media
- Create MastodonConnect.vue for instance URL input
- Create MastodonPreview.vue with Mastodon-styled post preview
- Update PlatformPreview.vue and Edit.vue to support Mastodon
- Add Mastodon config toggle in trypost.php

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-18 13:59:43 -03:00
Paulo Castellano
b8c4b704ad feat: Add Bluesky social media integration
- Add Bluesky platform to Platform and ContentType enums
- Create BlueskyController with custom auth flow (not OAuth)
- Create BlueskyPublisher service for posting via AT Protocol
- Add BlueskyConnect.vue page with handle/app password form
- Add BlueskyPreview.vue component for post preview
- Register Bluesky in PublishToSocialPlatform job
- Update Edit.vue with Bluesky logo and content type options
- Add Bluesky config toggle in trypost.php

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-18 13:33:54 -03:00
Paulo Castellano
adfba2755e feat: Add Pinterest integration and user language preferences
Pinterest Integration:
- Add Pinterest OAuth controller and routes
- Add PinterestPublisher service with support for pins, video pins, and carousels
- Add PinterestPreview component with board selector and content type options
- Add Pinterest content types enum (Pin, VideoPin, Carousel)
- Add Pinterest to Platform enum with proper configuration
- Support sandbox mode via PINTEREST_SANDBOX env variable
- Pass platform-specific data (boards) through PlatformPreview

Language Feature:
- Add languages table with migration
- Add Language model and seeder (en-US, pt-BR)
- Add LanguageCombobox component for profile settings
- Set default language (en-US) on user registration
- Add language_id foreign key to users table

UI Improvements:
- Refactor PlatformPreview to support contentTypeOptions, meta, and platformData props
- Move content type and board selectors into platform-specific preview components

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-18 13:10:01 -03:00
Paulo Castellano
1c798eb927 feat: Implement soft deletes for workspace hashtags and labels, and integrate content_type into the initial post platforms table creation. 2026-01-17 14:49:38 -03:00
Paulo Castellano
7867fcce02 feat: Implement real-time post status updates and configurable social platform enabling/disabling. 2026-01-16 12:36:52 -03:00