Commit Graph

3116 Commits

Author SHA1 Message Date
Gabriel Jablonski
3aca86aa43
feat(internal-chat): implement internal chat system for agents (#247)
* feat(internal-chat): implement internal chat system for agents (Phase 1+2 MVP)

Add a Slack/Discord-style internal messaging system for Chatwoot agents with
text channels (public/private), direct messages, reactions, typing indicators,
and real-time updates via ActionCable.

Backend:
- 6 database migrations (categories, channels, members, messages, attachments, reactions)
- 6 models under InternalChat:: namespace with validations and associations
- API controllers for categories, channels, messages, members, and reactions
- Pundit policies for authorization (public/private/DM access control)
- MessageCreateService, TypingStatusManager, DefaultChannelSetupService
- InternalChatListener for real-time broadcasting to channel members
- Event types for internal chat events
- Default category/channel setup for new and existing accounts

Frontend:
- Vuex store modules for channels, messages, and typing status
- API clients for channels and messages
- Vue 3 components: InternalChatLayout, ChannelSidebar, ChannelView,
  ChannelHeader, MessageList, MessageBubble, MessageEditor,
  EmojiReactionPicker, ReactionDisplay, TypingIndicator
- Sidebar integration with "Internal Chat" menu item
- ActionCable handlers for real-time message/reaction/typing events
- Route definitions and i18n translations

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test(internal-chat): add comprehensive specs for models, controllers, policies, services, and listener

- 6 model specs (74 examples) covering associations, validations, scopes, methods
- 5 request specs for all API controllers (categories, channels, messages, members, reactions)
- 4 policy specs testing authorization rules for all actions
- 3 service specs (DefaultChannelSetupService, MessageCreateService, TypingStatusManager)
- 1 listener spec testing real-time broadcasting for all event types
- 6 FactoryBot factories with traits for all InternalChat models

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): fix dispatcher mock in service specs and cursor pagination test

- Allow dispatcher.dispatch in service specs to handle Account.created
  callbacks from factory setup before asserting specific event dispatch
- Fix after-cursor pagination test by adding 1 second offset to avoid
  timestamp precision issues with iso8601 rounding

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): address CodeRabbit review — 7 critical security/correctness fixes

- Scope member creation through Current.account.users to prevent cross-account membership
- Scope member_ids in DM creation through Current.account to prevent cross-account invites
- Scope reaction message lookup through channel account to prevent cross-account access
- Fix Vuex store to commit messages array instead of response envelope
- Add UUID generation callback on Channel model (before_validation)
- Add channel access check to reaction deletion policy
- Validate parent_id belongs to same channel in MessageCreateService

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): address CodeRabbit round 2 + fix ChannelSidebar runtime error

- Re-throw error in fetchMessages instead of swallowing with empty array
- Wrap message + attachment creation in transaction for atomicity
- Fix factory to derive account from message (prevent cross-account fixtures)
- Guard listener against cross-account mismatch (not just missing records)
- Add cross-account regression tests to listener spec
- Fix ChannelSidebar computed properties to default to empty arrays

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(internal-chat): auto-setup default channels on account creation and migration

- Add after_create_commit :setup_internal_chat callback on Account model
- Add data migration to create default channels for existing accounts
- Make DefaultChannelSetupService convergent (find_or_create) instead of
  bail-on-exists, so it can sync new members on subsequent runs
- Fix specs to handle default category/channel created by callback

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): avoid Vuex state mutation in sort + align muted styling in fallback section

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): fix store module name mismatch — register as 'internalChat' not 'internalChatChannels'

Components dispatch to 'internalChat/get' but the module was registered
as 'internalChatChannels'. Also fix ActionCable handlers to use
'internalChat/messages/' nested module path.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* i18n(internal-chat): add pt-BR translations for internal chat feature

Backend: default_category_name ('Canais') and default_channel_name ('Geral')
Frontend: all 40+ keys translated to Brazilian Portuguese

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): handle ISO 8601 timestamps in MessageBubble and MessageList

The API returns created_at as ISO strings but messageTimestamp() expects
Unix seconds and MessageList used `* 1000`. Now handles both formats.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs(internal-chat): build swagger output for internal chat API endpoints

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs(internal-chat): register internal chat tags and paths in swagger index

Add tag definitions and path entries for all 5 internal chat resource
groups in swagger/index.yml and swagger/paths/index.yml. Rebuild output.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* i18n(internal-chat): add SIDEBAR.INTERNAL_CHAT key to pt-BR settings

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): comprehensive review fixes — backend and frontend

Backend:
- Add attachments to message API responses in both controllers
- Add internal_chat_channel_updated listener handler
- Include reactions in message event broadcast data

Frontend:
- Fix ActionCable dispatch paths to use correct action names
  (addMessageFromCable, updateMessageFromCable, deleteMessageFromCable)
- Connect typingUsers to internalChatTypingStatus store getter
- Fix message field references (edited → content_attributes.edited_at)
- Fix channel type comparisons (use 'private_channel'/'dm' strings)
- Add parent 'internal_chat' to sidebar activeOn array
- Increment unread_count on ActionCable message receive
- Add loadMore handler for cursor-based pagination
- Remove unused is-direct-message prop from InternalChatLayout

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(internal-chat): implement phases 3-5 — threads, mentions, notifications, polls, drafts

Phase 3 — Threads, Mentions, Notifications:
- MentionService: parse @user mentions, @all (admin only), generate notifications
- NotificationService: create notifications for channel messages (respects mute)
- Add internal_chat_message/mention notification types to Notification model
- ThreadPanel.vue: slide-out panel for threaded replies
- Integrate mentions + notifications into MessageCreateService

Phase 4 — Polls:
- 3 new migrations: polls, poll_options, poll_votes tables
- 3 new models: Poll, PollOption, PollVote with validations
- PollsController: create poll, vote, unvote with routes
- PollService: voting logic with multiple choice + revote support
- PollCreator.vue: modal for creating polls with options
- PollDisplay.vue: vote UI with progress bars and results
- Polls Vuex store module
- INTERNAL_CHAT_POLL_VOTED event type

Phase 5 — Drafts:
- 1 new migration: drafts table
- Draft model with validations
- DraftsController: full CRUD (replace stub)
- DraftsList.vue: list all user drafts with navigation
- Drafts Vuex store module with auto-save
- Draft route and sidebar integration

Phase 6 — Feature Flag:
- Add INTERNAL_CHAT feature flag to features.yml and featureFlags.js

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): fix API routing for drafts and polls, add poll voting ActionCable handler

- Fix draft API client to use channel-scoped PATCH/DELETE endpoints
- Create dedicated polls API client with correct poll-based endpoints
- Update polls store to use InternalChatPollsAPI with pollId-based voting
- Add ActionCable handler for internal_chat.poll.voted events
- Add thread and drafts routes to sidebar activeOn array
- Fix drafts store to pass channelId to API calls

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): fix poll response format and API client routing (review round 2)

- Return message with embedded poll data instead of raw poll response
- Add poll data to message_response in messages controller
- Create dedicated InternalChatPollsAPI client with correct endpoints
- Update PollDisplay.vue to read from message.poll or content_attributes.poll
- Use option.voted flag instead of checking voters array
- Add missing PERCENTAGE i18n key to pt-BR
- Remove unused currentUserId prop from PollDisplay

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): fix poll voting and draft lookup bugs (review round 3)

- Fix draft getter to use internal_chat_channel_id field name
- Split poll set_poll into vote/unvote variants — unvote doesn't need option_id
- Unvote finds user's vote by user_id across all poll options
- Fix ChannelView to extract pollId from message.poll before dispatching
- Fix unvote handler to not require optionId

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address CodeRabbit review feedback (round 3)

- Expose is_dm, favorited, muted on channel API responses
- Normalize poll cable updates into message-shaped patch
- Add file presence validation to MessageAttachment
- Remove duplicate mention notifications from MentionService
- Make data migration rollback safe (IrreversibleMigration)
- Update factory to include file by default

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: return 201 for channel creation and optimize DM lookup

- Return :created (201) instead of :ok (200) for channel creation
- Replace O(n) Ruby scan with SQL-based DM lookup using ARRAY_AGG

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address CodeRabbit review feedback (round 5)

- Broadcast channel event after create for real-time notifications
- Separate create/update strong params to prevent channel_type transitions
- Use strong params for typing_status input
- Replace find_by with detect on preloaded collections to fix N+1
- Preload attachments with blobs in show response

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address CodeRabbit review feedback (round 6)

- Serialize DM creation with advisory lock to prevent duplicates
- Broadcast channel deletion event for real-time UI updates
- Use strong params for mark_unread message_id
- Batch unread count computation to eliminate N+1 in index

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: eliminate N+1 in compute_unread_counts with single JOIN query

Replace per-membership COUNT loop with a single JOIN + GROUP BY query
that returns all unread counts in one database call.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): quality fixes, missing tests, and Playwright E2E setup

Addresses quality issues found during review and fills test coverage gaps
for the internal chat feature.

Backend fixes:
- Return 201 for all create endpoints (messages, categories, polls, reactions, members)
- Fix N+1 queries: replies.size, poll votes, category channels.count, votes.exists?
- Fix pagination has_more logic to check page size instead of total count
- Scope poll vote/unvote to current account (security fix)
- Add internal_chat.messages.deleted i18n key
- Use find_by! in mark_unread for proper 404 on non-members
- Guard time param parsing with rescue ArgumentError
- Align message response format between channels and messages controllers
- Switch notification service to ActionCable-only (avoid push/email crashes)

Frontend fixes:
- Fix pinned message detection (content_attributes.pinned, not message.pinned)
- Fix thread reply count key (replies_count, not thread_replies_count)
- Fix markUnread to pass message_id parameter
- Fix pagination: PREPEND_MESSAGES mutation instead of overwriting
- Fix typing status to read Vuex reactive state, not stale closure
- Fix deleteDraft argument shape (pass { channelId, draftId })
- Fix DM channel filtering (check both is_dm and channel_type)
- Fix DraftsList navigation to use correct channel ID key
- Wire PollCreator to poll button in MessageEditor
- Wire settings event handler on ChannelHeader
- Reset PollCreator isSubmitting on timeout

New RSpec tests (67 examples):
- Factories: polls, poll_options, poll_votes, drafts
- Model specs: Poll, PollOption, PollVote, Draft
- Controller specs: PollsController, DraftsController
- Service specs: PollService, NotificationService, MentionService

Playwright E2E setup (37 tests):
- Install Playwright with Chromium
- Auth helper with Devise Token Auth login flow
- 8 test suites: navigation, channels, messaging, DMs, reactions, threads, polls, mark-read-unread

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address CodeRabbit review feedback (round 7)

Backend:
- Use lambda for UUID default in channels migration
- Wrap poll creation in transaction for atomicity
- Preload replies in thread action to avoid N+1
- Broadcast replies_count + attachments in listener (match REST shape)
- Scope draft listing through accessible channels
- Key draft upserts/deletes by parent_id for thread drafts

Frontend:
- Remove duplicate poll methods from internalChatMessages.js (use internalChatPolls.js)
- Persist toggleMute/toggleFavorite to backend via updateMember API
- Clear active channel on DELETE_CHANNEL mutation
- Skip unread increment for active channel in ActionCable handler
- Filter archived channels from sidebar getters
- Fix ChannelHeader isArchived to check status === 'archived'
- Prevent duplicate reactions in ADD_REACTION mutation
- Merge poll data into existing content_attributes on cable updates
- Guard infinite scroll against duplicate loads
- Add response.ok() check in E2E auth helper, remove hardcoded account ID

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address CodeRabbit review feedback (round 8)

- Remove unused nested typingStatus module from internalChat store
- Add parent_id to draft uniqueness scope and migration index
- Exclude reaction creator from reaction_created broadcast
- Preload attachments and poll associations in thread/messages queries
- Handle `after` fetches with APPEND_MESSAGES mutation
- Wrap channel creation payloads under `channel` key in E2E helpers

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: rewrite Playwright E2E tests to use actual UI interactions

Completely rewrote all 8 E2E test suites to work with the live app:
- Test through actual UI interactions, not API bypass
- Use correct Portuguese (pt_BR) locale strings
- Use structural selectors matching real Vue component DOM
- Dynamic account ID from login response (no hardcoded values)
- 3 parallel workers, increased timeouts for reliability
- API calls only for preconditions (seeding test data)

29 tests passing across navigation, channels, messaging, DMs,
reactions, threads, polls, and mark-read-unread suites.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: use partial unique indexes for draft uniqueness with NULL parent_id

PostgreSQL treats NULL as distinct in unique constraints, so a composite
index on (user_id, channel_id, parent_id) allows duplicate root drafts.
Split into two partial indexes: one for root drafts (WHERE parent_id IS
NULL) and one for thread drafts (WHERE parent_id IS NOT NULL).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address CodeRabbit review feedback (round 9)

- Remove duplicate index on internal_chat_polls.internal_chat_message_id
  (keep only unique index)
- Add options validation in polls create (return 400 instead of 500)
- Add expiration check to unvote action (match vote behavior)
- Use strong params in messages update action

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address CodeRabbit review feedback (round 10)

- Change channel associations from destroy_async to destroy (FK
  constraints are ON DELETE RESTRICT, blocking async deletion)
- Remove unused internal_chat notification types and PRIMARY_ACTORS
  entry (notification service uses ActionCable only, no DB records)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address CodeRabbit review feedback (round 11)

- Scope category_id to current account in channels controller (security)
- Defer message-created event in poll creation until after transaction
- Change message associations from destroy_async to destroy (FK compat)
- Validate option belongs to poll in poll_service
- Use strong params for emoji in reactions controller

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address CodeRabbit review feedback (round 12)

Backend (9 fixes):
- Gate message update/destroy by channel accessibility in policy
- Guard content_attributes nil before merge in polls controller
- Fix after-cursor pagination to use limit() instead of last()
- Wrap revote in transaction for atomicity in poll service
- Make unvote option-specific for multi-choice polls
- Exclude own messages from unread count
- Make channel activity update monotonic (only write if newer)
- Include actor in message/reaction broadcasts (multi-tab support)
- Return 400 for empty member create instead of 201

Frontend (8 fixes):
- Show uncategorized channels even when categories exist
- Clear editor on channel switch when no draft exists
- Soft-delete messages in store (update in place, don't remove)
- Guard ThreadPanel against out-of-order fetch responses
- Replace hardcoded channel label with i18n key in DraftsList
- Add accessible name to settings button in ChannelHeader
- Add aria-label to search field in ChannelSidebar
- Make MessageBubble actions keyboard-accessible via focus-within

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address CodeRabbit review feedback (round 13)

- Fix keyword argument mismatch in reactions dispatch_reaction_event
- Add user_id to reaction cable broadcast for shape consistency with REST

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): quality fixes, expanded RSpec + Playwright E2E tests

Fix isArchived computed (checked .archived instead of .status), fix
ReactionDisplay user identification (.user?.id vs .user_id), update
17 spec assertions from :success to :created on create endpoints,
add 32 new RSpec examples (polls, drafts, services), and rewrite
8 Playwright E2E test files with correct selectors, proper test
isolation, and dynamic user ID discovery.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address CodeRabbit review feedback (round 14)

Prevent duplicate votes on same option in multi-choice polls with
explicit BadRequest guard. Add internal_chat webhook events to
ALLOWED_WEBHOOK_EVENTS so users can subscribe to them.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: include poll data in ActionCable broadcast for poll messages

Extract base_message_data helper and enrich message_event_data with
poll options when the message has an associated poll, ensuring
realtime subscribers receive the same poll data as REST API clients.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address CodeRabbit review feedback (round 15)

Backend: wrap single-choice revotes in transaction, capture member
tokens before channel destroy, exclude own messages from unread count,
strip attachments from deleted messages, enrich poll broadcast payload.

Frontend: use getCurrentRole getter, fix public-results poll display,
sync thread replies via store, add close button a11y, pass option_id
to unvote API, pass parent_id to deleteDraft API.

Models: handle nil last_read_at for new members, skip content
validation for attachment-only messages, align PollService guards
with controller, change category dependent to nullify.

Swagger: add attachments to message schema, fix create status to 201.
E2E: remove fragile waitForTimeout.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): fix 21 UX/functional issues

Address 21 UX gaps discovered during product testing:

Sidebar & Navigation:
- Fix search icon overlap, extend search to description + DM members
- Add create channel/DM/category buttons and modals
- Show DM member names instead of null
- Include members data in channel index API for DMs

Message Interactions:
- Add delete confirmation dialog
- Implement inline message editing with cancel support
- Toggle emoji reactions (add/remove)
- Support multiple pinned messages with click-to-scroll
- Prevent thread replies from appearing in main chat
- Fix reply count live updates
- Hide pin button on thread messages
- Improve deleted message styling with greyed-out card
- Replace spinner with skeleton loading
- Add markdown toolbar (bold/italic/code)
- Fix thread editing and add vote/unvote handlers

Features & Polish:
- Implement channel settings slide-over panel
- Fix thread loading not affecting main channel spinner
- Fix poll creation field name mismatch with backend API
- Fix drafts: show channel names, handle DM navigation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): use Dialog modal for delete confirmation

Replace window.confirm with the project's Dialog component for
message delete confirmation, providing a consistent UI experience.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address CodeRabbit review feedback (round 16)

- Require content field in message update OpenAPI schema

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address CodeRabbit review feedback (round 17)

- Sanitize advisory lock SQL with sanitize_sql_array
- Use semantic button for pinned message banner
- Add aria-label to ChannelSettings close button
- Add type="button" to all ChannelSettings buttons
- Gate channel/DM/category creation to admins
- Replace hardcoded 'Direct Message' with i18n key

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address CodeRabbit review feedback (round 18)

- Wrap DM creation payload in channel key for consistency
- Replace raw text in category select with i18n key
- Add IME composition guard to prevent premature send

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(internal-chat): UX round 2, rich editor, team members, drag-and-drop

- Reduce sidebar spacing between search bar and drafts
- Fix search icon overlapping placeholder text
- Replace inline category form with Dialog modal
- Add collapsible sidebar sections with localStorage persistence
- Add drag-and-drop channels across categories (admin-only, vuedraggable)
- Replace textarea editor with WootWriter ProseMirror rich text editor
- Replace regex markdown rendering with shared MessageFormatter
- Wire draft auto-save pipeline with WootWriter (3s debounce watcher)
- Add team + agent selection when creating private channels
- Auto-add all agents when creating public channels
- Sync team members to linked channels via TeamMember callback
- Fix member list not loading on first settings panel open
- Complete PT-BR translations for all internal chat strings

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(internal-chat): UX round 3, Enter-to-send, mentions, copy link, poll modal

- Send with Enter (not Cmd+Enter), Shift+Enter for newlines
- Enable @mentions via WootWriter suggestions plugin
- Refocus editor after sending a message
- Copy link to message button in hover toolbar
- Poll creator refactored to Dialog with confirm-discard on close
- Channel type uses Switch instead of dropdown
- Category uses components-next Select instead of native select
- Skeleton loading: only on initial load, spinner for pagination
- Scroll position preserved when loading older messages
- Mute/Favorite buttons fixed (store members updated after fetch)
- Add/remove channel members after creation (admin-only)
- Save draft immediately when switching channels
- Settings sidebar remembers open/closed state via localStorage
- Search icon overlap fixed (increased padding)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): DM settings, copy updates, input refocus, member UX

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): member edit for private only, emoji overflow, reaction tooltips

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): thread count sync, scroll loading, copy link, thread/settings exclusivity

- Fix thread reply count doubling (remove duplicate INCREMENT_REPLY_COUNT from sendThreadReply, cable handles it)
- Fix copy link button (use window.location.origin + pathname as fallback)
- Hide poll button in thread editor
- Add "Also send in #channel" checkbox for thread replies
- Increase scroll threshold for loading older messages (100px instead of 0)
- Track and stop loading when oldest message reached
- Thread and settings panels are mutually exclusive
- Refocus editor after send with delayed focus

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): scroll to linked message via ?messageId= query param

Read messageId from route query on mount, scroll to and highlight the
target message after messages load, then clean the query param.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): prevent editor from becoming unfocusable after send

Root cause: passing disabled prop to WootWriter applies pointer-events-none
and ProseMirror does not re-enable contenteditable when disabled returns to
false. Fix: never disable the WootWriter, use a local isSending guard to
prevent double-sends. Refocus 300ms after send for ProseMirror state reload.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): simplify send guard, no artificial timeout

Content is cleared immediately before emit, so canSend naturally
returns false (empty content). No isSending guard needed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): align poll option remove buttons vertically

Increase padding to p-1.5 and add flex-shrink-0 for consistent sizing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): close poll modal after creating poll

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): poll option X button alignment, discard modal on submit

- Button uses explicit 34px height matching input, no items-center
- Reset form before closing dialog so hasUnsavedChanges is false

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): close settings sidebar when clicking reply

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): update poll UI after voting, fix re-vote error

Vote/unvote actions now dispatch updateMessageFromCable with the API
response to update poll state locally. Pass channelId to enable this.
Clicking an already-voted option correctly triggers unvote.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): include poll data in message response, add timer and voters

Backend: message_response now includes poll data (options, votes, voted
status, voters for public polls) via eager-loaded poll association.
This fixes polls not rendering after page reload.

Frontend PollDisplay:
- Countdown timer showing time remaining until poll closes
- Read-only state when expired (div instead of button, no hover)
- Voter names shown below each option (public polls or admin)
- Prefer content_attributes.poll over message.poll for fresh data

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): include channel_id in poll voted cable broadcast

The poll_event_data was missing internal_chat_channel_id, so the
frontend cable handler could not route the update to the correct
channel's message store.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): poll vote highlight, typing off, reaction broadcast, translations

- Preserve per-user voted flags when merging cable poll broadcast
- Send typing_off after 3s of no typing activity
- Include internal_chat_channel_id in reaction event broadcasts
- Fix reaction deleted handler to also check channel_id field
- Simplify "also send in" copy (works for both channels and DMs)
- Add PT-BR translation for ALSO_SEND_IN_CHANNEL

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(internal-chat): unified reaction popover with all emoji groups

Clicking any reaction badge opens a single popover showing all reactions
grouped by emoji with user names. Current user can remove their own
reaction via X button. Replaces per-reaction popover with unified view.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): wire close DM button to archive and navigate home

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(internal-chat): close DM via hidden flag on channel membership

Add hidden boolean to channel_members table. Close DM sets hidden=true
via member update API. Sidebar filters out hidden DMs. New messages on
a DM channel automatically unhide all members via listener callback.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): reaction popover, user names, file upload support

- Include user name in reaction API response (was missing)
- Redesign reaction popover: flat list with emoji + name per row,
  aligned X button for removing own reactions
- Add file upload: paperclip button opens file picker, attached files
  shown as chips with remove, sent via FormData with message
- Store action and API client support files parameter

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): reaction user names, unreact button, attachment rendering

- Include user name in reactions across all endpoints (messages_controller,
  listener base_message_data)
- Make unreact X button always visible (bg-n-alpha-2 background)
- Render message attachments as downloadable links with paperclip icon

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(internal-chat): image preview for attachments in messages and editor

Messages: images render inline with max-h-60, non-images as download links.
Editor: image files show thumbnail preview, non-images show file icon + name.
Remove button as floating circle on top-right corner of each attachment.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): attachment preview matches conversation pattern

- File previews show name + size (e.g. "2 MB") in a horizontal card
- Image thumbnails as 32px squares, non-images show document emoji
- Remove button is a visible X icon (not a floating circle)
- Layout matches AttachmentsPreview from conversation ReplyBox

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): auto-detect image file type from MIME on upload

MessageCreateService now detects file type from content_type instead of
defaulting to :file. Images are correctly tagged as :image so they
render inline in message bubbles.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): include file_url in cable broadcast, fix filename display

Listener attachment_event_data now includes file_url so attachments
render correctly on real-time messages without page refresh.
MessageBubble extracts filename from URL or falls back to file_type+ext.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): pin attachments, edit members modal, settings persistence

- Skip content validation when pinning/unpinning (fixes pin on file-only messages)
- Add EditMembersModal with search, add, and remove members for private channels
- Fix settings sidebar always opening: @close now calls handleToggleSettings
  which updates localStorage, not just sets ref to false

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): fix X buttons for attachment remove and reaction unreact

Replace Icon component with inline SVG cross for reliable rendering.
Both attachment remove and reaction unreact buttons now show a visible
X icon at all times with proper vertical alignment.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): allow any user to pin messages, not just sender/admin

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): restore Icon component for X buttons (size-4 in size-6 container)

SVG inline approach didn't render. Reverted to Icon i-lucide-x with
larger sizes (size-4 icon in size-6 button) which renders reliably.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): use p-1 + size-4 pattern for X buttons (matches message toolbar)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(internal-chat): thread indicator on messages from threads, allow pinning all

- Show "Thread" badge with icon on messages that have parent_id,
  clicking it opens the parent thread
- Remove parent_id restriction from canPin, any non-deleted message
  can be pinned

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): thread indicator, poll close loop, thread navigation

- Hide thread indicator inside thread panel (inThread prop)
- Open parent thread when clicking thread badge on messages with parent_id
- Fix PollCreator infinite close loop (handleClose no longer calls
  dialogRef.close, since Dialog already triggered the close)
- Look up parent message in store when opening thread from indicator

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(internal-chat): poll duration translations and clickable switch labels

- Duration options use i18n keys (EN + PT-BR: 24 horas, 7 dias, etc.)
- Multiple choice and Public results switches toggle by clicking label

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(internal_chat): enhance message handling and search functionality

- Added broadcasting of typing off events in InternalChatListener.
- Included member user IDs in channel data for better context.
- Updated message model to allow optional sender association.
- Implemented team mention expansion in MentionService to include team members.
- Enhanced message creation service to store mentioned user IDs in content attributes.
- Introduced a new SearchService for searching channels, DMs, and messages.
- Updated API responses to include has_unread_mention flag for channels.
- Added tests for user deletion behavior in internal chat, ensuring message preservation and reaction handling.
- Improved draft model to allow coexistence of root and thread drafts.
- Added unique indexes for drafts to prevent duplicate entries.
- Implemented foreign key constraints with appropriate delete behaviors for internal chat models.

* feat(internal-chat): swagger docs, webhook events, search UX improvements

Add Swagger documentation for drafts, polls, and search endpoints.
Wire internal_chat_message_deleted and internal_chat_channel_updated
webhook events to the UI and listener. Improve search empty state with
min-chars hint and friendly no-results message. Update CLAUDE.md to
include pt_BR translations.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(internal-chat): add draft count display in channel sidebar

* chore: remove playwright config and dependencies

* feat(internal-chat): polish UX, swagger updates, and migration consolidation

- Editor toolbar shortcuts (@ and #) with instant popover trigger,
  including accent-insensitive matching and wider conversation popover
- Localized last activity time and inbox name on conversation preview cards
- Thread + main interplay: also-send-in-channel mirror, parent_id filter,
  per-message conversation link, hidden buttons inside thread view,
  reactions update across both lists, scroll-to-message behavior
- Search service uses f_unaccent for messages, channel names and user names
  via dedicated GIN trigram functional indexes
- Renamed InternalChat::ProGating to InternalChat::Limits with neutral
  semantics
- Consolidated 17 internal chat migrations into 3 (tables, default channels,
  unaccent search) and added a rake task to ensure the f_unaccent function
  exists before db:schema:load
- Swagger paths and definitions updated to match the current state of the
  feature (also_send_in_channel, status codes, pro-required responses,
  hidden member flag, search meta fields, etc.)

* fix(internal-chat): use Rake task augmentation for db:schema:load hook

The previous `Rake::Task['db:schema:load'].enhance(...)` guarded by
`task_defined?` silently no-op'd in CI when the rake file loaded before
ActiveRecord's rake_tasks block ran. Re-opening `db:schema:load` via
Rake's `task name => deps` DSL augments the existing task regardless of
load order, ensuring the f_unaccent function is created before schema.rb
references it.

* fix(internal-chat): enhance db:schema:load from Rakefile after load_tasks

Adding the prereq inside lib/tasks/internal_chat_search.rake (via either
`Rake::Task#enhance` or task DSL augmentation) was being silently dropped
in CI, presumably due to load order between application rake files and
ActiveRecord's `rake_tasks` block. Moving the `enhance` to the Rakefile
itself, after `Rails.application.load_tasks`, guarantees both
`db:schema:load` and `db:internal_chat:ensure_search_functions` are
defined before the prereq is added.

Also leaves a debug `puts` in the task body so future regressions are
visible from CI logs.

* chore(internal-chat): add diagnostic logging to f_unaccent hook

* fix(internal-chat): install f_unaccent on all envs iterated by db:schema:load

Rails' `db:schema:load` in development env iterates over BOTH the
development and test databases (see
`ActiveRecord::Tasks::DatabaseTasks.each_current_environment`), but our
hook was only installing the function on the currently-connected
database. CI defaults to development env (no `RAILS_ENV` set), so the
function landed on `chatwoot_dev` while `chatwoot_test` remained
without it, causing the schema load to fail when creating the functional
indexes against the test DB.

The hook now mirrors the same iteration logic and installs the function
on every relevant config, restoring the original AR connection
afterwards.

* fix(internal-chat): align listener spec with current broadcast payload

- internal_chat_message_created now emits two broadcasts (the message
  itself plus an automatic typing_off), so the spec switches to
  `allow`/`have_received` to assert the message broadcast without caring
  about the additional typing_off call.
- internal_chat_reaction_created payload uses `message_id`, not
  `internal_chat_message_id`. Update the spec expectation to match.

* chore(internal-chat): remove redundant DSL augmentation in rake task

* fix(internal-chat): harden gates, kill N+1s and reduce race risk

Closes review findings raised on the internal chat PR:

- Restrict role mass-assignment in ChannelMembersController so only
  account administrators can promote new members to channel admin.
- Wrap private-channel create/unarchive in a Postgres advisory lock per
  account so concurrent requests can no longer bypass the freemium limit.
- Replace `replies.size` and `votes.size` (per-broadcast queries) with
  `replies_count` / `votes_count` counter caches.
- Make `update_channel_activity` an atomic compare-and-set update so
  concurrent message creates can never regress `last_activity_at`.
- Optimize `Poll#total_votes_count` to use the cached column and eager-
  loaded options instead of a per-poll `votes.count` query.
- Add `internal_chat_messages.account_id` foreign key (`ON DELETE
  CASCADE`) to prevent orphan rows.
- Escape HTML in `ChannelSidebar.highlightMatch` to close a v-html XSS
  via incomplete tags in message search snippets.
- Cleanup `typingOffTimer` on `ChannelView` unmount.
- Add stable sort to `getChannelsByCategory` (alphabetical) and
  `getDMChannels` (last activity) to prevent UI reorder thrash.
- Localize `PollDisplay` time-remaining strings (en + pt-BR).
- Add specs covering the 90-day search history filter and the search
  controller endpoint, plus regenerate the consolidated migration
  with the new columns and FK.

* docs(swagger): note role mass-assignment restriction on channel members

Document that the `role` field on the channel member create payload is
silently coerced to `member` for callers that are not account
administrators, matching the controller behavior introduced in the
previous commit.

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 13:50:15 -03:00
Gabriel Jablonski
dba5379d5e
fix(whatsapp): include baileys and z-api in multi-attachment split (#256)
The ReplyBox multi-attachment split only covered Twilio, Cloud, and
360Dialog providers. Baileys and Z-API were added later and were
missing from the check, causing extra attachments to be silently
dropped. Use the generic isAWhatsAppChannel flag instead.
2026-04-07 20:13:29 -03:00
Gabriel Jablonski
8728db8869
fix(conversation): restore scroll-to-bottom after switching conversations (#250)
The scheduled-messages refactor (9a05ff524) split setActiveChat into
two branches but dropped the .then() callback that emits
SCROLL_TO_MESSAGE for normal conversation loads (no messageId).
Without this event scrollToBottom() never fires after messages load,
leaving the scroll at the top and triggering infinite history fetch.
2026-03-26 12:46:53 -03:00
Gabriel Jablonski
d120c25917
fix(groups): restrict enable CTA to superadmin users (#248)
Non-superadmin users now see a "contact your administrator" message
instead of the "how to enable" action button on group disabled banners.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 13:01:02 -03:00
gabrieljablonski
8fcef79847 Merge branch 'chatwoot/develop' into chore/merge-upstream-4.12.0 2026-03-20 00:27:45 -03:00
gabrieljablonski
d154edf8ea feat: add utility analyzer messages for improved classification 2026-03-19 23:59:18 -03:00
gabrieljablonski
d3ce2a4cf9 Merge branch 'main' into chore/merge-upstream-4.12.0 2026-03-19 23:24:55 -03:00
Cayo P. R. Oliveira
c6bfd1eed3
feat: schedule messages recurrence (#240)
* feat(scheduled-messages): add recurring scheduled messages

Implements the recurring scheduled messages feature allowing agents to
configure recurrence rules when scheduling messages, with automatic
creation of subsequent scheduled messages after each send.

Backend:
- RecurringScheduledMessage model with JSONB recurrence_rule validation
- RecurrenceCalculatorService for next occurrence date calculation
- RecurrenceDescriptionService for human-readable rule descriptions
- CreateNextOccurrenceService for auto-creating child ScheduledMessages
- RecurringScheduledMessagesController with CRUD operations
- RecurringScheduledMessagePolicy for authorization
- Modified SendScheduledMessageJob to handle recurrence after send
- Updated due_for_sending scope to exclude resolved conversations
- ActionCable events for real-time updates
- Activity message i18n (en + pt-BR)

Frontend:
- RecurrenceDropdown.vue with contextual shortcut options
- RecurrenceCustomModal.vue for custom recurrence configuration
- RecurringScheduledMessageItem.vue for sidebar display
- Integration into ScheduledMessageModal.vue
- Updated ScheduledMessages.vue with recurrence section and filtering
- Vuex store module for recurring scheduled messages
- API client for CRUD operations
- WebSocket handlers in actionCable.js
- recurrenceHelpers.js utility functions
- i18n keys for en and pt-BR

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(recurring-messages): fix creation and edit visibility

- Fix API payload key mismatch (snake_case vs camelCase) in modal submit
- Add status: 'active' to recurring creation payload
- Fix strong params to permit recurrence_rule array fields (week_days)
- Cast string values from strong params to integers for JSONB validation
- Show RecurrenceDropdown when editing (remove isEditing gate)
- Populate recurrenceRule from scheduled message's recurring parent
- Include recurring_scheduled_message_id and recurrence_rule in
  scheduled message jbuilder response

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(recurring-messages): fix dropdown toggle and locale tag errors

- Use DropdownItem :click prop instead of @click to use the injected
  closeMenu from DropdownContainer context (default slot doesn't
  expose toggle)
- Normalize locale from pt_BR to pt-BR for Intl.DateTimeFormat
  compatibility in RecurringScheduledMessageItem

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(recurring-messages): add separator line and expandable history

- Add border separator between recurring messages section and
  pending/draft messages, matching the history section separator
- Replace static 'N enviadas' counter with clickable toggle that
  expands to show individual sent/failed child messages with
  status badges and formatted timestamps

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(recurring-messages): add click-to-navigate on sent children

Make sent child messages in recurring message history clickable.
Clicking navigates to the actual message in the conversation using
the messageId query param, same pattern as ScheduledMessageItem.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(recurring-messages): allow editing recurring messages

- Add edit button to RecurringScheduledMessageItem (active only)
- Transform recurring message into scheduledMessage-compatible shape
  with recurring_scheduled_message_id set, so the modal reuses
  the existing update path
- Handle edge case of removing recurrence from a recurring message
  (cancels series without trying to update a non-existent standalone)
- Sent history is preserved by the backend update action

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(recurring-messages): show recurrence field without date selection

Remove v-if="scheduledDate" gate so the recurrence dropdown is
always visible in the modal. Falls back to today's date for
contextual shortcut labels when no date is selected yet.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(recurring-messages): ensure recurrence visible on edit and add pending_scheduled_message to API

- Add pending_scheduled_message to recurring_scheduled_message jbuilder
  so REST API data matches WebSocket push_event_data
- Add fallback in openEditRecurringModal to find pending child from
  scheduled_messages array when pending_scheduled_message is absent
- Add same fallback in nextSendLabel computed

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(scheduled-messages): add sections for Drafts and Pending messages in the UI

* refactor(scheduled-messages): merge RecurringScheduledMessageItem into ScheduledMessageItem

Consolidate the recurring message card into the existing
ScheduledMessageItem component instead of maintaining a separate
component. The unified component detects recurring messages via
recurrence_rule and conditionally shows:
- Recurrence description header with repeat icon
- Next send time label
- Expandable sent/failed children history with click-to-navigate
- Stop button (replaces delete) with confirmation modal
- Active/completed/cancelled status badges

Delete the now-unused RecurringScheduledMessageItem.vue.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(scheduled-messages): use blue badge for active, keep green for sent

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(scheduled-messages): remove draft and pending sections from UI and update recurrence title

* fix(recurring-messages): use Teleport for recurrence dropdown

Replace DropdownContainer with Teleport-based floating dropdown so
options render outside the modal. Fixes:
- Dropdown no longer enlarges the modal or causes scrolling
- Dropdown closes before Custom modal opens (no overlap)
- Auto-detects available space and opens above/below trigger

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(recurring-messages): recalculate next date when recurrence rule changes

When editing a recurring message and changing the recurrence rule,
the pending occurrence date is now validated against the new rule.
If the user-provided date doesn't match (e.g. Thursday removed from
weekly days), the system computes the next valid date using
RecurrenceCalculatorService instead of blindly using the old date.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(scheduled-messages): resolve deprecated onClose and disabled type warnings

- Replace :on-close prop with @close event on woot-modal components
- Cast hasTemplate computed to boolean to fix disabled prop type check

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(scheduled-messages): replace remaining deprecated on-close props

- ScheduledMessages.vue delete confirm modal
- RecurrenceCustomModal.vue modal

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(recurring-messages): address review feedback on PR #240

- Fix v-if/v-else bug hiding message list behind resolved warning
- Fix occurrences_sent incrementing on failed sends (skip_increment flag)
- Fix compute_next_valid_date using .min instead of .max
- Fix Vuex delete action to update state on cancel (not remove)
- Use atomic update_counters for occurrences_sent increment
- Add safe Date.iso8601 parsing with rescue in should_complete?
- Add null: false to occurrences_sent migration column
- Fix pt-BR accent: Recorrencia → Recorrências
- Use I18n.with_locale(account.locale) for all activity messages
- Fix N+1 in jbuilder partials (Ruby filtering + eager loading)
- Add interval >= 1 validation to RecurrenceCustomModal isValid
- Validate recurrence_rule presence when status is active
- Add ISO8601 date format validation for end_date
- Add unknown_agent i18n key for fallback

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(recurring-messages): wrap create/update in transactions, clean pending on deactivation

- Wrap create and update flows in ActiveRecord transactions
- Move attachment purge after save! to prevent data loss on validation failure
- Destroy pending children when status transitions to non-active
- Fixes critical bug where stopping recurrence could leave armed pending messages

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(recurring-messages): prevent monthly/yearly day-of-month drift

Store the original intended day in recurrence_rule JSONB as month_day
(for monthly) and year_day/year_month (for yearly). The calculator
now uses these stored values instead of @last_date.day, preventing
drift after short months cap the day (e.g., Jan 31 → Feb 28 → all
subsequent months stuck on 28).

Backend:
- RecurrenceCalculatorService: use rule[:month_day] for monthly and
  rule[:year_day]/rule[:year_month] for yearly calculations
- Controller: permit and cast the new integer keys

Frontend:
- recurrenceHelpers: yearly shortcuts include year_day/year_month
- RecurrenceCustomModal: emit month_day for monthly day_of_month
  rules and year_day/year_month for yearly rules

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(recurring-messages): i18n description services, align due_for_sending scope

Backend:
- RecurrenceDescriptionService: replace hardcoded English with I18n.t()
  calls; accept locale parameter and use I18n.with_locale
- RecurringScheduledMessage model: pass account locale to description service
- ScheduledMessage: align due_for_sending? instance method with scope by
  checking conversation status (open/pending)

Frontend:
- buildRecurrenceDescription: use t() i18n function instead of manual
  isPt locale branching
- Add DESCRIPTION i18n keys to en/conversation.json and pt_BR/conversation.json
- Update RecurrenceDropdown and ScheduledMessageItem callers to pass t

i18n:
- Add recurring_scheduled_messages.description.* keys to en.yml and pt_BR.yml

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(recurring-messages): keep pending child when deactivating recurrence

When editing a recurring message to disable recurrence while setting a
future send date, the pending child message is now preserved instead of
being destroyed. This allows a 'final send' without creating new
recurrences (the send job already guards with recurring&.active?).

Backend:
- Add update_pending_on_deactivation: updates pending child's
  scheduled_at or creates a final pending occurrence
- Replace destroy_all in update's non-active branch

Frontend:
- activeRecurringMessages now includes non-active recurring messages
  that still have a pending child (pending_scheduled_message)
- Stop button hidden for already-cancelled recurring messages
- inactiveRecurringMessages excludes messages with pending children

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(recurring-messages): prevent removing recurrence from existing recurring message

Once a scheduled message has recurrence, the 'Does not repeat' option is
hidden from the RecurrenceDropdown when editing. This avoids edge cases
where deactivating recurrence leaves the message in an ambiguous display
state.

- RecurrenceDropdown: add hideNoRepeat prop, filter NO_REPEAT from shortcuts
- ScheduledMessageModal: pass hideNoRepeat when isEditingRecurring
- Revert update_pending_on_deactivation (no longer reachable from UI)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(recurring-messages): address CodeRabbit review feedback

- ScheduledMessage#push_event_data: expose recurring_scheduled_message_id
  in ActionCable payloads so frontend correctly classifies children
- RecurringScheduledMessagePolicy: add agent_bot? check for parity with
  ScheduledMessagePolicy
- RecurrenceCalculatorService: guard against nil/empty week_days
- Factory: bind inbox and account to conversation to prevent cross-account
  flakiness in specs

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(recurring-messages): update schema to enforce non-null constraint on occurrences_sent

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: gabrieljablonski <contact@gabrieljablonski.com>
2026-03-19 22:51:14 -03:00
Cayo P. R. Oliveira
8ffdc16faf
fix(schedule): enhance attachment handling in scheduled message modal (#239)
* fix(schedule): enhance attachment handling in scheduled message modal

* fix(schedule): enable attachment removal in scheduled message updates
2026-03-19 22:45:21 -03:00
Cayo P. R. Oliveira
a06354c6b2
feat: scheduled message shortcuts (#238)
* feat(scheduled-messages): add predefined time shortcuts for scheduling

Replace the manual date/time picker with predefined day and time period
chip selectors for faster message scheduling.

Day shortcuts: Today, Tomorrow, This weekend (Sat), Next week (Mon),
Next weekend (next Sat), Next month (1st), Custom (date picker fallback).

Time period shortcuts: Morning (8:00), Afternoon (13:00), Evening (18:00).

Each day chip shows the corresponding date (dd/MM) in secondary color.
Past time periods for 'Today' are automatically disabled.
The existing date picker is preserved as the 'Custom' option.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* refactor(scheduled-messages): use dropdown selectors and remove header

Replace chip/button selectors with native <select> dropdowns for day
and time period selection. Remove the 'Date and time to send' header
from the modal since the dropdown labels serve as placeholders.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(scheduled-messages): close datepicker on confirm and click-outside

Removed manual open/close state management and @click.stop that was
blocking click-outside detection. Added confirm prop so the picker
has an explicit OK button and auto-closes properly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(scheduled-messages): initialize custom mode when editing existing schedule

When editing a scheduled message, the ScheduleDateShortcuts component
now detects the pre-existing datetime and opens in Custom mode with
the datepicker pre-filled, preserving the original date and time.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(scheduled-messages): use locale-aware date format in shortcuts

Replace hardcoded dd/MM format with Intl.DateTimeFormat using
navigator.language, matching the existing locale-aware pattern
in DatePickerHelper.js. Removes unused date-fns format import.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(scheduled-messages): use app locale for date formatting

Replace navigator.language with the i18n app locale for shortcut date
labels and datepicker calendar. Add getDatePickerLang helper that
generates locale-aware day/month names via Intl.DateTimeFormat.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(scheduled-messages): normalize locale tag to BCP 47 format

Chatwoot uses underscore locale tags (pt_BR) but Intl.DateTimeFormat
requires BCP 47 hyphens (pt-BR). Add toBcp47 normalizer to prevent
RangeError: invalid language tag.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(scheduled-messages): locale-aware time period display

Add formatHour helper using Intl.DateTimeFormat so time periods
show 8:00/13:00/18:00 in pt-BR and 8:00 AM/1:00 PM/6:00 PM in en.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* test(scheduled-messages): add unit tests for scheduleDateShortcutHelpers

Cover getShortcutDate (weekday/Saturday/Sunday edge cases),
applyTimePeriod, isTimePeriodPast, formatShortDate, formatHour,
getDatePickerLang, and getDayShortcutOptions including locale
normalization for underscore tags like pt_BR.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(scheduled-messages): sync customDateTime when modelValue changes in custom mode

Keep DatePicker in sync when the parent changes modelValue while
already in Custom mode (e.g. switching between scheduled messages).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(scheduled-messages): add aria-labels to schedule dropdown selects

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(scheduled-messages): use empty string instead of null for dateTimeError

Avoids Vue prop validation warning since ScheduleDateShortcuts
declares dateTimeError as a String prop.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* refactor(scheduled-messages): simplify to 3 fixed schedule shortcuts

Replace two dropdown selectors (6 day options × 3 time periods) with
3 pre-computed clickable shortcut buttons:
- Tomorrow morning (08:00)
- Tomorrow afternoon (13:00)
- Monday morning (08:00)

Each shortcut shows the exact calendar date and time for clarity.

Special Sunday rule: 'Monday' points to next week's Monday since
'Tomorrow' already covers the immediate Monday.

The 'Custom' option with full DatePicker is preserved.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(scheduled-messages): Gmail-style list design for schedule shortcuts

Replace chip buttons with full-width clickable rows in a bordered
container, matching Gmail's 'Schedule send' dialog pattern:
- Label on left, formatted date/time on right in gray
- Calendar icon for the custom date/time option
- Subtle border separators between rows
- Selected state with blue highlight

Also improves date formatting from '15/03' to '15 de mar.' using
month short names for better readability.

Updates i18n:
- PT_BR: 'Amanhã à tarde', 'Escolher data e hora'
- EN: 'Choose date and time'

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(scheduled-messages): improve modal layout and datepicker positioning

- Add 'Schedule send' / 'Programar envio' section header above the
  schedule shortcuts for clear visual identification
- Move attachment/template controls next to the message editor so
  content-related actions stay grouped together
- Move datepicker outside the bordered shortcut container to avoid
  cramped positioning; add rounded-xl and proper text sizing
- Add i18n key SCHEDULE_LABEL (en: 'Schedule send', pt_BR: 'Programar envio')

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* refactor(schedule): replace popup datepicker with inline calendar

Replace the popup-style vue-datepicker-next with an inline calendar
(same approach used by the snooze feature), rendering the calendar
directly within the modal for a more intuitive UX.

- Switch DatePicker to inline mode (no popup/z-index issues)
- Add disablePastTimes validation (prevents selecting past times)
- Full-width responsive calendar with scoped deep styles
- Remove unused DATETIME_PLACEHOLDER and DATETIME_FORMAT i18n keys

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(schedule): replace datepicker with natural language input

Replace the broken inline datepicker with a natural language text input
powered by chrono-node, similar to Chatwoot's upcoming snooze UX.

Users can now type dates naturally:
- EN: 'tomorrow at 2pm', 'next friday morning', 'in 3 hours'
- PT: 'amanhã às 14h', 'próxima sexta de manhã', '20 de março às 10h'

Changes:
- Add chrono-node dependency for natural language date parsing
- Add preProcessDateInput() to normalize PT time expressions (8h→8:00,
  de manhã→8:00, à tarde→13:00, de noite→18:00)
- Add parseNaturalDate() with locale-aware parsing (PT/EN)
- Add formatFullDateTime() for parsed date preview display
- Replace DatePicker with text input + real-time parsed date preview
- Show green checkmark when date is valid, amber warning if in the past,
  hint text if input is unrecognizable
- Remove unused getDatePickerLang() and vue-datepicker-next import
- Remove unused DATETIME_PLACEHOLDER/DATETIME_FORMAT i18n keys
- Update i18n: CUSTOM label, placeholder, hint, and past-date warning
- Update tests: 40 tests covering new functions (was 29)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(schedule): improve PT natural language preprocessing

Normalize accent-less input common in casual Brazilian Portuguese:
- 'amanha' → 'amanhã', 'sabado' → 'sábado', 'proxima' → 'próxima'
- 'as' → 'às' before digits or time-of-day words (e.g. 'as 19h')
- Support 'pela manhã/tarde/noite' and 'no período da manhã/tarde/noite'

Previously 'Amanhã as 19h' failed because chrono-node requires 'às'
(with accent) as a time connector. Now all common casual PT patterns
work reliably.

Tests: 50 passing (was 40)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(schedule): use forwardDate to always parse weekdays as upcoming

chrono-node defaults to the *most recent* occurrence of a weekday,
so 'sexta' on Monday returned last Friday (past). Adding
{ forwardDate: true } makes it always return the next occurrence.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(schedule): add datepicker fallback and dual-language parsing

- Add discrete calendar icon button next to text input that toggles
  an inline vue-datepicker-next with date+time selection
- Try both chrono.pt and chrono (EN) parsers, pick the one that
  matches more of the input text — supports mixed-language input
  like 'quarta 10am' or 'friday às 14h'
- Insert 'às' connector between PT weekday names and bare numbers
  so 'quarta 10' parses correctly (chrono.pt requires the connector)
- Add DATEPICKER_TOOLTIP i18n key (EN + PT_BR)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(schedule): toggle between text input and datepicker views

When calendar button is clicked, hide the natural language text input
and show the inline datepicker full-width. A small 'Type a date and
time' link below the calendar lets users switch back to text input.
Calendar button is vertically centered with the input field (size-[34px]
matches input height).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(schedule): align calendar button with text input height

Use self-stretch instead of fixed size so the button stretches to
match the input height in the flex row, eliminating misalignment.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(schedule): adjust margin for custom text input in date shortcuts

* fix(schedule): use popup datepicker with datetime and confirm

Replace the inline datepicker toggle with a popup DatePicker that
opens directly from the calendar button. Uses type='datetime' with
confirm mode so users can pick both date and time. On confirm, the
selected datetime populates the natural language text field. The text
input and calendar button are always visible side by side.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(schedule): hide seconds column in datepicker

Add :show-second='false' to only show hour and minute selection.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(schedule): adjust DatePicker width for better responsiveness

* refactor(schedule): use locale for parser priority and add aria-label

- parseNaturalDate now uses locale to call the matching parser first
  (chrono.pt for PT, chrono for EN) before falling back to the other,
  removing the eslint-disable comment for unused locale param
- Add aria-label to the natural language date input for screen readers

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* chore: remove leftover planning files

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: gabrieljablonski <contact@gabrieljablonski.com>
2026-03-19 22:43:51 -03:00
Cayo P. R. Oliveira
9a05ff5247
feat: find scheduled message (#237)
* feat(scheduled-messages): scroll to sent message from sidebar

- Expose message_id in JBuilder serialization and push_event_data
- Add HIGHLIGHT_MESSAGE bus event for in-page message highlighting
- Add 'Go to message' button on sent scheduled messages in sidebar
- Enhance onScrollToMessage to fetch messages around target when not in DOM
- Extend Message.vue highlight to work with bus events (not just route query)
- Add i18n keys for EN and pt-BR

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* refactor(scheduled-messages): make sent card clickable instead of button

Replace the 'Go to message' button with a clickable card. The entire
sent scheduled message card now has cursor-pointer, hover highlight,
and a tooltip — clicking anywhere on it scrolls to the message.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(scheduled-messages): address PR review feedback

- Use camelCase value for HIGHLIGHT_MESSAGE bus event ('highlightMessage')
- Show toast alert when message not found after fetch or on fetch error
- Use the MESSAGE_NOT_FOUND i18n key that was previously unused

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(scheduled-messages): use messageId query param for find message

Replace direct bus event emission with route navigation using
?messageId= query param, reusing the same proven mechanism used by
search results and copy-message-link.

Changes:
- ScheduledMessageItem: router.replace with ?messageId= instead of
  emitting SCROLL_TO_MESSAGE directly
- ConversationView: handle ?messageId= on same-conversation (was
  previously skipped), fetch messages around target and scroll
- MessagesView: clean up ?messageId= from URL after scroll/error

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(scheduled-messages): add toast feedback for find message

Show a persistent "Searching for message..." toast while fetching,
auto-dismissed on success. Show "Message not found" error toast if
the message cannot be located.

Uses usePendingAlert for the loading state in both ConversationView
(initial fetch) and MessagesView (fallback fetch).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: prevent scroll overshoot when navigating to message

Remove the immediate fetchPreviousMessages() call after
scrollIntoView({ behavior: smooth }). The fetch was prepending
messages above the target while the smooth scroll animation was
still running, shifting the DOM and causing the scroll to stop
short of the target message. The scroll event handler will
naturally trigger message loading when the user scrolls up later.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* chore(scheduled-messages): remove redundant clearMessageIdFromRoute calls

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-19 22:35:04 -03:00
Gabriel Jablonski
a996b920e8
feat: group conversations (#228)
* feat: add group and conversation types to contacts and conversations, and implement conversation group membership model

* chore: add factory and specs for conversation group member model

* chore: add group type checks and associations for contacts and conversations

* refactor: remove scopes from ConversationGroupMember model

* refactor: remove scopes from ConversationGroupMember model specs

* refactor: enhance conversation type migration with concurrent indexing

* feat: add is_active index and scopes to ConversationGroupMember model

* feat: implement GroupConversationHandler for managing group conversations

* feat: add group_type attribute to contact creation

* fix: update WHATSAPP_CHANNEL_REGEX to allow up to 20 digits to handle group jid

* feat: handle group JID format in remote_jid method

* chore: update group contact info when finding or creating group contact

* chore: refactor and implement contact message handling and message creation logic for baileys single contact conversation

* feat: implement group message handling and metadata fetching in WhatsApp service

* chore: add spec for group type handling in contact creation for individual and group contacts

* chore: add specs for test scopes in conversation group members

* chore: update documentation for sender phone extraction in group conversation handler

* chore: move GroupConversationHandler concern to correct dir

* chore: implement specs for recipient_id handling to individual and group contacts

* chore: add group message handling specs for incoming messages

* chore: enhance group message handling to prevent race conditions

* chore: add group_metadata method to with error handling

* chore: add test for sending messages to group recipients in WhatsappBaileysService

* chore: raise error for unsuccessful response in group_metadata method

* chore: adds tests for group metadata retrieval and error handling

* chore: refactor build_sender_contact_attributes to avoid double call methods

* chore: update error handling for attachment download failure in message creation

* chore: optimize update_contact_info method to use compact hash for updates

* chore: simplify find_or_create_sender_contact method return values

* chore: rename group and individual contact message handlers

* chore: remove pointless comments from group contact message handler methods

* chore: refine sender JID extraction logic to remove unnecessary checks

* chore: remove phone number in spec for group contact attributes

* chore: implement sync_group route

* chore: implement get group_members route

* fix: sync_group participants creation handling

* chore: update contact avatar handling in group message processing

* chore: move sync_group functionality for conversation model

* feat: add sync_group action to ConversationsController and route

* fix: set contact name to phone in group message processing

* chore: refine group member retrieval logic in sync_group service and view

* feat: implement group participants update handling

* feat: implement group updates handling and localization for group activities

* chore: add handling for group membership requests and icon changes

* chore: add authorization for sync_group action in ContactsController

* chore: add sync_group endpoint specs for contact management

* chore: add authorization for sync_group action in ConversationsController

* chore: add specs for sync_group endpoint in ConversationsController

* chore: refactor index action in GroupMembersController for improved conversation filtering

* chore: add request specs for group_members endpoint in ContactsController

* chore: add specs for sync_group method in Conversation model

* chore: add specs for sync_group method in Channel::Whatsapp model

* chore: remove comments in find_or_create_group_conversation method

* chore: add specs for Contacts::SyncGroupService to validate group contact behavior

* chore: add specs for Whatsapp::BaileysHandlers::GroupsUpdate to validate group updates

* chore: add specs for Whatsapp::BaileysHandlers::GroupParticipantsUpdate to handle group participant actions

* chore: add fallback for identifier when contact has no phone_number in SendOnWhatsappService

* chore: add specs for group membership request and icon change handling in MessagesUpsert

* chore: add specs for sync_group method to handle group metadata and participant updates

* chore: update sync_group method to retrieve group members and adjust JSON response

* chore: update conversation query to filter by group type in GroupMembersController

* chore: update conversation creation in group_members_controller_spec to specify conversation_type as group

* chore: update find_or_create_group_conversation to include pending conversations

* chore: refactor sync_group method and enhance specs for group conversation handling

* feat: add GroupEventHelper module for managing group activities and contacts

* chore: refactor group contact inbox and conversation creation methods in group handlers

* chore: remove unnecessary check for blank participant contacts in sync_group_members method

* feat: implement message receipt update handling for WhatsApp integration

* chore: resolve rubocop rule for update_last_seen_at method

* chore: update swagger with endpoints for syncing group information and listing group members

* chore: integrate Contacts::SyncGroupService in group members controller, enhance error handling and update swagger

* chore: include participant information in reaction and quoted message keys for send message in group conversations

* chore: enhance whatsapp_baileys_service with participant handling for message keys

* feat: add skill for writing RSpec tests in the project

* fix: update recipient_id logic to directly use contact identifier for group contacts

* chore: implement group stub message handling for membership requests and icon changes

* fix: update whatsapp inbox source_id validation regex spec

* chore: fix spec for contact syncing group

* chore: remove readTimestamp handling and related tests for message read updates in group

* Cayo oliveira/cu 86af01932/4 backend gerenciamento dos grupos (#221)

* feat: add is_active index and scopes to ConversationGroupMember model

* feat: implement GroupConversationHandler for managing group conversations

* feat: add group_type attribute to contact creation

* fix: update WHATSAPP_CHANNEL_REGEX to allow up to 20 digits to handle group jid

* feat: handle group JID format in remote_jid method

* chore: update group contact info when finding or creating group contact

* chore: refactor and implement contact message handling and message creation logic for baileys single contact conversation

* feat: implement group message handling and metadata fetching in WhatsApp service

* chore: add spec for group type handling in contact creation for individual and group contacts

* chore: add specs for test scopes in conversation group members

* chore: update documentation for sender phone extraction in group conversation handler

* chore: move GroupConversationHandler concern to correct dir

* chore: implement specs for recipient_id handling to individual and group contacts

* chore: add group message handling specs for incoming messages

* chore: enhance group message handling to prevent race conditions

* chore: add group_metadata method to with error handling

* chore: add test for sending messages to group recipients in WhatsappBaileysService

* chore: raise error for unsuccessful response in group_metadata method

* chore: adds tests for group metadata retrieval and error handling

* chore: refactor build_sender_contact_attributes to avoid double call methods

* chore: update error handling for attachment download failure in message creation

* chore: optimize update_contact_info method to use compact hash for updates

* chore: simplify find_or_create_sender_contact method return values

* chore: rename group and individual contact message handlers

* chore: remove pointless comments from group contact message handler methods

* chore: refine sender JID extraction logic to remove unnecessary checks

* chore: remove phone number in spec for group contact attributes

* chore: implement sync_group route

* chore: implement get group_members route

* fix: sync_group participants creation handling

* chore: update contact avatar handling in group message processing

* chore: move sync_group functionality for conversation model

* feat: add sync_group action to ConversationsController and route

* fix: set contact name to phone in group message processing

* chore: refine group member retrieval logic in sync_group service and view

* feat: implement group participants update handling

* feat: implement group updates handling and localization for group activities

* chore: add handling for group membership requests and icon changes

* chore: add authorization for sync_group action in ContactsController

* chore: add sync_group endpoint specs for contact management

* chore: add authorization for sync_group action in ConversationsController

* chore: add specs for sync_group endpoint in ConversationsController

* chore: refactor index action in GroupMembersController for improved conversation filtering

* chore: add request specs for group_members endpoint in ContactsController

* chore: add specs for sync_group method in Conversation model

* chore: add specs for sync_group method in Channel::Whatsapp model

* chore: remove comments in find_or_create_group_conversation method

* chore: add specs for Contacts::SyncGroupService to validate group contact behavior

* chore: add specs for Whatsapp::BaileysHandlers::GroupsUpdate to validate group updates

* chore: add specs for Whatsapp::BaileysHandlers::GroupParticipantsUpdate to handle group participant actions

* chore: add fallback for identifier when contact has no phone_number in SendOnWhatsappService

* chore: add specs for group membership request and icon change handling in MessagesUpsert

* chore: add specs for sync_group method to handle group metadata and participant updates

* chore: update sync_group method to retrieve group members and adjust JSON response

* chore: update conversation query to filter by group type in GroupMembersController

* chore: update conversation creation in group_members_controller_spec to specify conversation_type as group

* chore: update find_or_create_group_conversation to include pending conversations

* chore: refactor sync_group method and enhance specs for group conversation handling

* feat: add GroupEventHelper module for managing group activities and contacts

* chore: refactor group contact inbox and conversation creation methods in group handlers

* chore: remove unnecessary check for blank participant contacts in sync_group_members method

* chore: update swagger with endpoints for syncing group information and listing group members

* chore: integrate Contacts::SyncGroupService in group members controller, enhance error handling and update swagger

* fix: update recipient_id logic to directly use contact identifier for group contacts

* chore: implement group stub message handling for membership requests and icon changes

* fix: update whatsapp inbox source_id validation regex spec

* chore: fix spec for contact syncing group

* fix: optimize update_last_seen_at method to use update_columns

* feat: Implement full frontend and backend support for group conversations

- Added PRD for group conversations detailing frontend and backend requirements.
- Created new Baileys TypeScript definitions for group-related functions.
- Renamed `conversation_type` to `group_type` in the database and updated all references.
- Implemented API serialization for `group_type` in conversation and contact responses.
- Developed Vuex store module for managing group members.
- Created UI components for group management, including group creation, member management, and metadata editing.
- Integrated @mention functionality for group conversations and real-time updates via ActionCable.

* feat: [US-001] - Rename conversation_type to group_type on conversations

- Add migration to rename column and indexes
- Update Conversation model enum to group_type
- Update GroupConversationHandler concern
- Update controllers (contacts, group_members)
- Update all backend specs

* chore: mark US-001 complete, update progress log, fix rubocop annotation

* feat: [US-002] - Serialize group_type fields in API responses

* feat: [US-003] - Add group_type filter to conversations index

* feat: [US-004] - Add group_type to filter_keys.yml and FilterService

* feat: US-005 - Backend group creation endpoint

- Add POST /api/v1/accounts/:account_id/groups endpoint
- Add Groups::CreateService to orchestrate Baileys group creation
- Extend WhatsappBaileysService and BaseService with group management methods
- Add routes for group members, metadata, invite, and join requests
- Returns 403 when agent lacks inbox access, 422 when provider is unavailable

* feat: US-006 - Backend add/remove members and role management endpoints

- Add create/destroy/update actions to GroupMembersController
- Delegate group management methods from Channel::Whatsapp to provider_service
- create adds members via Baileys and creates ConversationGroupMember records
- destroy removes a member by ID and sets is_active false
- update promotes/demotes a member and updates their role

* feat: US-007 - Backend group metadata update endpoint

- Add PATCH /contacts/:id/group_metadata endpoint
- Updates group subject via Baileys and syncs contact name
- Updates group description via Baileys and syncs additional_attributes.description
- Returns 422 when provider is unavailable

* feat: US-008 - Backend invite link management endpoints

- Add GET /contacts/:id/group_invite to retrieve current invite code/url
- Add POST /contacts/:id/group_invite/revoke to revoke and get new invite code/url
- Returns 422 when provider is unavailable

* feat: US-009 - Backend join request management endpoints

- Add GET /contacts/:id/group_join_requests to list pending join requests
- Add POST /contacts/:id/group_join_requests/handle to approve/reject requests
- Uses request_action param to avoid conflict with Rails reserved params[:action]
- Returns 422 when provider is unavailable

* feat: US-010 - Extend MentionService for contact mentions

- Extract mention://contact/ID/Name URIs from message content
- Store mentioned contact IDs in message.content_attributes[mentioned_contacts]
- Existing user/team mention handling unchanged

* feat: US-011 - Frontend API clients for all group endpoints

- Add app/javascript/dashboard/api/groupMembers.js
- Exports 11 methods: getGroupMembers, syncGroup, createGroup, updateGroupMetadata,
  addMembers, removeMembers, updateMemberRole, getInviteLink, revokeInviteLink,
  getPendingRequests, handleJoinRequest

* feat: US-012 - Frontend Vuex store module groupMembers

- Add groupMembers store module with fetch, sync, addMembers, removeMembers, updateMemberRole actions
- Add SET_GROUP_MEMBERS and SET_GROUP_MEMBERS_UI_FLAG mutation types
- Register module in store index

* feat: US-013 - Frontend i18n keys for group features

- Add groups.json with keys for group info, filter, creation modal, metadata editing, invite link, member management, join requests, and mention dropdown
- Register groups.json in i18n locale en/index.js

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: US-014 - Frontend group_type filter in ConversationBasicFilter

- Add chatGroupTypeFilter state, getter, mutation, and action to conversations store
- Add getChatGroupTypeFilter getter
- Add group_type param to ConversationApi.get()
- Add Type filter section to ConversationBasicFilter with All/Individual/Group options
- Persist group_type to UI settings under conversations_filter_by.group_type
- Restore group_type from UI settings on page load
- Include groupType in conversationFilters and pass as group_type param to API

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: US-013 - Frontend — i18n keys for group features (en + pt-BR)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat: [US-014] - Frontend — add group_type filter to ConversationBasicFilter

All implementation was already in place from prior work:
- ConversationBasicFilter.vue has Type section with All/Individual/Group options
- ChatList.vue handles group_type in conversationFilters and restores from UI settings
- Store has setChatGroupTypeFilter action, getChatGroupTypeFilter getter
- API maps groupType → group_type query param

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat: US-015 - Frontend — add group_type to advanced filter system

- Add GROUP_TYPE to CONVERSATION_ATTRIBUTES in filterHelper.js
- Add group_type filter definition in provider.js (components-next)
- Add group_type to legacy advancedFilterItems/index.js and filterAttributeGroups
- Add group_type to automationHelper conditionFilterMaps
- Add group_type to customViewsHelper getValuesForFilter
- Add group_type options to ChatList setParamsForEditFolderModal
- Add GROUP_TYPE i18n key in en and pt_BR advancedFilters.json

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat: US-016 - Frontend — GroupContactInfo basic display

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat: US-017 - Frontend — GroupContactInfo sync button

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat: US-018 - integrate GroupContactInfo in ContactPanel

- Import GroupContactInfo component
- Conditionally render GroupContactInfo when group_type === 'group'
- Keep ContactInfo for individual conversations (no regression)
- Dynamic sidebar title: 'Group' for groups, 'Contact' for individual
- contact_notes and contact_attributes accordion sections unchanged

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat: US-019 - Frontend — group creation UI modal

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat: US-020 - Frontend — member management UI in GroupContactInfo

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat: US-021 - Frontend — group metadata editing UI

Add inline editing for group name, description, and avatar in GroupContactInfo:
- Click group name to edit inline, save on Enter/blur
- Click description to edit inline with textarea, save on blur
- Click avatar to open file picker for upload via contacts/update
- Loading states on all fields during save
- Success/error alerts for all operations
- updateGroupMetadata action added to groupMembers store

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat: US-022 - Frontend — invite link management UI

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat: US-023 - Frontend — join request management UI

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat: US-024 - Frontend — group message bubbles: sender name with color

- Add sender name display above incoming message bubbles in group conversations
- Deterministic color per sender using AVATAR_COLORS palette (name.length % 6)
- Sender name hidden for consecutive messages from the same sender
- Individual conversation bubbles unchanged
- Pass groupWithPrevious and isGroupConversation props through MessageList → Message

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat: US-025 - Frontend — group message bubbles: sender avatar

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat: Add Ralph Wiggum AI agent script for managing tool execution and progress tracking

* feat: US-026 - Frontend — @mention dropdown for group conversations

- Create TagGroupMembers.vue component for group member mention suggestions
- Modify Editor.vue: add isGroupConversation/groupContactId props, render
  TagGroupMembers for group non-private context
- Modify ReplyBox.vue: compute isGroupConversation and groupContactId from
  currentChat, pass to WootMessageEditor
- @ mention plugin isAllowed now triggers for group conversations
- In individual conversations or private notes, existing behavior unchanged

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat: US-027 - Frontend — mention rendering in group message bubbles

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat: US-028 - Frontend ActionCable handler for contact.group_synced event

- Backend: Include group_members data in contact.group_synced ActionCable payload
- Frontend: Register contact.group_synced handler in ActionCableConnector
- Frontend: Add setGroupMembers action to groupMembers store for direct commits
- Tests: ActionCable handler spec + groupMembers store spec for new action

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat: Update progress tracking for group conversations feature - mark tests as passing

* fix: sender click case mismatch and filter dropdown spacing

- Message.vue: use case-insensitive comparison for sender type check
  (Contact.push_event_data returns 'contact' but SENDER_TYPES.CONTACT is 'Contact')
- ConversationBasicFilter.vue: replace last:mt-4 with flex-col gap-4
  for consistent spacing between all three filter sections

* fix: four bugs found during manual testing review

- ContactPanel.vue: fix i18n key GROUP.INFO.SIDEBAR_TITLE → GROUP.SIDEBAR_TITLE
- groupMembers.js API: fix syncGroup HTTP method GET → POST to match backend route
- group_members_controller.rb: remove SyncGroupService from index action
- filterHelpers.js: add missing group_type case to getValueFromConversation

* docs: update progress with bug fix learnings

* chore: implement group creation functionality in UI components

* chore: add copy invite link functionality and update UI components

* feat: US-041 - Backend — ensure group_type is set on existing contacts and conversations

GroupConversationHandler#update_group_contact_info now sets group_type: :group
on contacts that are incorrectly typed as individual.

GroupConversationHandler#find_or_create_group_conversation updates existing
conversation's group_type to :group if it is currently :individual.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* chore: mark US-041 as complete

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat: US-029 - i18n keys for You badge and group settings (en + pt-BR)

All i18n keys already existed from prior iterations. Verified presence
and updated PRD status.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat: US-030 - fix Baileys API route/method mismatches

Fix on_whatsapp to dig('data') before accessing first element.
Update spec stubs to match { data: [...] } response envelope.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat: US-031 - group_leave, group_setting_update, group_join_approval_mode methods

All methods, delegates, and error handling already implemented.
Verified specs pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat: US-032 - persist group settings, invite code, and profile picture during sync

Add try_update_group_avatar to fetch and attach group profile picture
during sync_group. Update spec stubs for profile-picture-url endpoint.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat: US-033 - GroupSettingsController with leave, update, toggle

Controller and routes already implemented. Verified rubocop passes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat: US-034 - remove inbox_contact_id from provider_config and jbuilder

Already removed in prior iterations. Verified no references remain.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat: US-035 - refactor TagGroupMembers to phone_number matching

Already implemented. Verified excludePhoneNumber prop and filtering.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat: US-036 - remove InboxContact.vue and settings tab

Already removed in prior iterations. Verified no references remain.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat: US-037 - add You badge in GroupContactInfo member list

Already implemented with isOwnMember check and blue badge styling.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat: US-038 - fix inline edit for group name and description

Already implemented with phone number normalization. Verified code.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat: US-039 - group settings section UI with toggles

Already implemented. Settings toggles, API calls, and i18n verified.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat: US-040 - leave group UI with confirmation and auto-resolve

Already implemented. Leave button, confirmation, and API call verified.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat: US-050 - Create GroupMember model and migration

New group_members table with group_contact_id, contact_id, role, is_active.
Unique index on (group_contact_id, contact_id). Associations added to Contact.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat: US-064 - Helper method to find channel from group contact

Add Contact#group_channel to decouple channel lookup from conversations.
Update GroupMembersController and GroupSettingsController to use it.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat: US-052 - Update GroupConversationHandler to use GroupMember

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat: US-057 - Update GroupMembersController to query GroupMember

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat: US-058 - Update GroupSettingsController to not depend on conversations

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat: US-060 - Update group_members jbuilder views

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat: US-059 - Remove group_members association from Conversation model

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat: US-051 - Remove ConversationGroupMember model and table

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* chore: mark all stories complete, update progress

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(groups): real-time group panel, avatar refresh on icon change, editable name/description

- Add group_type to Conversations::EventDataPresenter#push_data and
  Contact#push_event_data so WebSocket events carry the field, enabling
  the frontend to switch to GroupContactInfo in real-time
- Update handle_icon_change_stub to call try_update_group_avatar with
  force: true, purging the cached avatar and fetching the new one
- Add force parameter to try_update_group_avatar to support re-fetching
- Remove isInboxAdmin gate from name/description editing in
  GroupContactInfo so any user can click to edit (server validates)

* fix(groups): rewrite SyncGroupService and simplify group metadata channel lookup

- Rewrite SyncGroupService to use contact.group_channel directly instead
  of iterating conversations; find or create a conversation for sync
- Simplify GroupMetadataController to use @contact.group_channel instead
  of querying conversations; remove local contact/attribute updates since
  the Baileys API handles persistence via webhook events

* feat(groups): resolve conversations when inbox phone leaves or is removed

- Add resolve_conversations_if_inbox_left to GroupParticipantsUpdate
- Resolves all open/pending conversations when the inbox phone number
  is removed from or leaves a group

* feat(groups): add paginated member list with infinite scroll

- Backend: add pagination to GroupMembersController (page/per_page,
  default 10, ordered admins first); add meta with pagination info
  to the jbuilder response
- Frontend: update groupMembers API to accept page param; add
  APPEND_GROUP_MEMBERS and SET_GROUP_MEMBERS_META mutations; implement
  paginated fetch with append and isFetchingMore flag in store

* feat(groups): support Ctrl+Click on group message sender to open in new tab

- navigateToGroupSender now accepts the event and checks for
  Ctrl/Cmd+Click to open the sender contact in a new tab

* chore(i18n): update leave group confirmation text in en and pt_BR

* fix(groups): handle phone format differences in You badge and admin detection

- Extract phonesMatch helper that compares last 8 digits as fallback,
  handling Brazilian 9th digit discrepancy (e.g. +5587988465072 vs
  +558788465072)
- Apply to both isOwnMember and isInboxAdmin computed properties

* feat(groups): auto-sync members on mount, show existing members immediately

- On mount, fetch existing DB members first so they display instantly
- Then silently attempt a background sync to refresh from WhatsApp
- If sync fails (e.g. WhatsApp disconnected), existing members remain
  displayed without any user-facing error

* fix(groups): pin own member on first page and return inbox phone in meta

The "You" badge was not appearing because the inbox's own member could be
missing from the first paginated page in large groups (admins sorted first).

Backend:
- Pin the inbox's own member at the top of page 1 regardless of sort order
- Return inbox_phone_number in the group members meta response
- Use last-8-digit SQL fallback for Brazilian 9th-digit phone mismatches

Frontend:
- Use meta.inbox_phone_number for the inboxPhone computed
- Fix declaration order to satisfy no-use-before-define lint rule

* fix(groups): fix member action dropdown clipped by overflow container

The promote/demote/remove dropdown menu was invisible because the member
list had `overflow-y-auto max-h-80`, clipping any absolutely-positioned
dropdown rendered inside it.

- Remove overflow container from member list; let the sidebar scroll
- Replace scroll-based infinite loading with IntersectionObserver on a
  sentinel element for cleaner pagination trigger
- All member action logic (promote, demote, remove) was already wired;
  the dropdown is now visible on hover

* fix(groups): keep member action dropdown visible when menu is open

The opacity-0/group-hover classes on the action menu wrapper caused the
DropdownMenu to become invisible as soon as the mouse left the row.
Now the wrapper stays fully opaque while the menu is active.

* fix(groups): move clickaway to member list wrapper to prevent instant close

v-on-clickaway was bound to every member's action div individually.
Clicking the three-dot button on one member fired closeMemberMenu from
all other members' clickaway handlers, closing the menu instantly.

Moved the directive to the single member list container instead.

* feat: add WhatsApp mention conversion (incoming + outgoing)

- New MentionConverterService for bidirectional mention handling
- Incoming: converts @phone/mentionedJid to mention://contact/ URIs
- Outgoing: extracts mention://contact/ URIs into WhatsApp mentions array
- Supports @everyone/todos group mentions
- WhatsApp renderer preserves mention display text instead of raw URI

* fix: preserve mention display text in WhatsApp renderer

mention:// URIs now render as display name text instead of the raw URL
when converting markdown to WhatsApp format

* feat: add @everyone mention option in group conversations

- Everyone item shown at top of mention dropdown
- Searchable by 'all', 'todos', 'everyone' keywords
- i18n keys added for en and pt-BR

* refactor: use Switch component for group settings toggles

- Add disabled prop to Switch component
- Replace custom toggle buttons in GroupContactInfo with Switch
- Loading spinner shown alongside toggle while toggling

* feat(whatsapp): add group sync status tracking (group_left, group_last_synced_at)

* feat(whatsapp): hide group management UI when group_left is true

* fix(groupMembers): include inbox phone number in group members state and sync event

* feat(whatsapp): wrap group settings and leave in Accordion component

* feat(groupMembers): handle group creator modification errors and update error messages

* feat(groupMembers): enhance invite link functionality and clean up UI state after copying

* refactor: remove sync_group functionality from conversations and related specs

* feat(GroupContactInfo): implement scroll-based loading for group members

* docs(swagger): add group API endpoints and remove conversation sync_group

- Remove dead conversation/{id}/sync_group swagger entry and file
- Update group_members.yml with pagination params, POST operation, and $ref schema
- Add swagger for: group_members_member (PATCH/DELETE), group_metadata,
  group_invite, group_invite_revoke, group_join_requests,
  group_join_requests_handle, group_settings, group_settings_leave,
  group_settings_toggle_join_approval, groups/create
- Add group_member schema definition
- Add Groups tag to application tag_groups
- Register all 12 group endpoints in paths/index.yml

* feat(WhatsappBaileysService): enhance mention handling by replacing @DisplayName with @lid/@phone in outgoing text

* feat(groups): move group sync to background job with 15-min cooldown

- Create Contacts::SyncGroupJob that checks group_last_synced_at
  before calling SyncGroupService (skips if < 15 min)
- Controller sync_group now enqueues the job and returns 202 Accepted
- Delete sync_group.json.jbuilder (no longer needed)
- Frontend sync action is fire-and-forget; results via ActionCable
- Auto-trigger sync on conversation select and panel mount
- Remove manual sync button from GroupContactInfo

* fix: show group members list even after leaving group\n\nKeep the members section visible in read-only mode when\ngroup_left is true. Admin actions (add member, promote,\ndemote, remove) remain hidden. Pending Join Requests and\nAdvanced Options also stay hidden.

* fix: disable group name/description/avatar editing when group_left is true

* fix: remove @all mention and fix Enter key in group mention dropdown\n\n- Remove the @all/everyone special mention from TagGroupMembers since\n  no channel provider currently supports mentioning all participants\n- Fix Enter key sending message instead of inserting selected mention\n  in group conversations. The root cause was Editor.vue only emitting\n  toggleUserMention=true for private notes (isPrivate), leaving\n  ReplyBox unaware the group mention dropdown was open. Now also\n  emits for isGroupConversation.\n- Add TagGroupMembers spec covering filtering, exclusion, and emission"

* fix: address PR review feedback for group conversations

- Fix nil safety in group_invites and group_join_requests controllers
  by replacing group_conversation.inbox.channel with @contact.group_channel
- Add before_action guard in group_members_controller to validate
  contact is a group with identifier before create/update/destroy
- Persist metadata locally in group_metadata_controller after
  provider calls (subject -> name, description -> additional_attributes)
- Add server-side allow_group_creation? check in groups_controller
- Add word boundary to mention regex to prevent matching inside words
- Remove useless catch clauses in groupMembers store (try/finally only)
- Default groupType to [] in customViewsHelper to prevent crash
- Fix swagger parameter name mismatch (contact_id -> id) across
  all group endpoint YML files for consistency

* fix: address PR #228 review feedback - strong params, guards, and safety fixes

* fix: dispatch real-time events for Baileys group participant and metadata updates

Both group-participants.update and groups.update handlers were updating
backend data (GroupMember records, Contact attributes) but never
dispatching ActionCable events, leaving the frontend member list and
group metadata stale until manual sync.

Changes:
- Add dispatch_group_synced_event helper to GroupEventHelper concern
- Dispatch CONTACT_GROUP_SYNCED after participant add/remove/promote/demote
- Dispatch CONTACT_GROUP_SYNCED after group subject/description/settings changes
- Frontend: onContactGroupSynced also dispatches contacts/updateContact
  to refresh group name, description and settings in the sidebar

* fix: enhance member menu positioning and close behavior on sidebar scroll

* feat: implement group property updates and enhance toast notifications

* fix: update WhatsApp channel regex to allow optional hyphenated numbers

* feat: implement group admin functionalities including leave, update properties, and toggle join approval

* refactor: simplify group message handling by removing metadata fetching and syncing methods

* chore: remove raph files

* feat: update Portuguese translations for 'Read More' and 'Insert Read More' phrases

* feat: enhance group admin functionalities with join approval and member add modes

* feat: enhance group join request handling by adding removal of handled requests and updating pending join requests

* feat: restrict message sending in announcement mode groups

When a Baileys WhatsApp group has announcement mode enabled (announce=true),
only admin members can send messages. This adds:
- Frontend: disabled editor + banner for non-admin inbox in announcement groups
- Backend: validation in SendOnWhatsappService to reject messages
- Shared phone helper utility extracted from GroupContactInfo
- i18n keys for en and pt_BR

* feat: add group sync job enqueueing and improve avatar update handling

* feat: add functionality to reset invite link and confirm member addition restrictions

* feat: update group name extraction logic to handle nil values

* feat: add inbox admin status handling and update related components

* feat: remove group conversation resolution on leave action

* feat: enhance group sender avatar interaction with tooltip and cursor pointer

* feat: add force option to SyncGroupJob and update related specs

* feat: enhance invite link handling and avatar update logic in group conversations

* chore: remove prd.json

* fix: change group sender name display from block to inline-block for better layout

* feat: add group members loading check and fetch logic in MessagesView and ReplyBox components

* feat: allow id and firstUnreadId props to accept both Number and String types
feat: add vOnClickOutside import to Editor component
feat: enhance Portuguese translations for integrations and settings
fix: change button color in GroupContactInfo component from green to teal

* feat: soft-disabled group conversations with activity tracking

Groups start in a soft-disabled state by default when using Baileys.
Chatwoot still creates group conversations but does not process every
incoming message. Instead, Baileys accumulates group messages and sends
periodic groups.activity webhook events to update last_activity_at.

Backend:
- Add WHATSAPP_GROUPS_ENABLED env var and groups_enabled? class method
- Send groupsEnabled in Baileys connection setup
- Create groups.activity handler to update conversation last_activity_at
- Gate group message processing behind groups_enabled? check
- Expose groups_enabled via inbox API

Frontend:
- Add warning banner with CTA to app.fazer.ai on disabled group conversations
- Disable reply editor for non-private-note mode when groups disabled
- Add i18n strings for en and pt_BR

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: use method for groups disabled banner action to avoid window scope issue

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: broadcast conversation update after groups.activity event

update_columns bypasses ActiveRecord callbacks, so the ActionCable
broadcast was never triggered when last_activity_at changed. Dispatch
a CONVERSATION_UPDATED event explicitly so the sidebar updates in
real-time.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: show unread dot for soft-disabled group conversations with activity

Since soft-disabled groups don't create messages, unread_count is
always 0 and the standard badge won't show. Detect unread state by
comparing last_activity_at > agent_last_seen_at for these groups
and display a teal dot indicator instead of a count badge.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: clear unread dot when agent opens soft-disabled group conversation

The update_last_seen endpoint skipped updating agent_last_seen_at when
there were no unread messages (the throttle path). For soft-disabled
groups that never create messages, this meant the dot indicator could
never be cleared. Add an unseen_activity? check that bypasses the
throttle when last_activity_at > agent_last_seen_at.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: group avatar upload to provider and fix icon change sync

- Route avatar upload through GroupMetadataController to push to WhatsApp
  provider before saving locally
- Add update_group_picture to baileys service and base service
- Fix buildContactFormData crash when social_profiles is undefined
- Make try_update_group_avatar public so GROUP_CHANGE_ICON stub handler
  can call it from outside the service class

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: update specs for group conversations feature changes

- Add groupsEnabled param to setup_channel_provider and handle_channel_error WebMock stubs
- Add group-request-participants-list stub for sync_group tests
- Add group_type to push_event_data expected hash
- Set last_activity_at in throttle tests to prevent unseen_activity? bypass
- Update sync_group delegation expectation to include soft: false
- Stub groups_enabled? in group message handling tests
- Update WhatsApp source_id regex expectation for group contact IDs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: add settings file for additional directories configuration

* chore: undo unrelated changes

* chore: remove planning doc, fix migration version, fix swagger param consistency

- Remove planejamento-chat-interno.md (unrelated planning document)
- Fix CreateGroupMembers migration API version from 7.0 to 7.1
- Fix swagger.json: normalize group endpoint paths from {contact_id} to {id}
  to match YAML sources and existing contact sub-resource conventions

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: CayoPOliveira <cayoproliveira@gmail.com>
Co-authored-by: Cayo P. R. Oliveira <cayo@fazer.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-19 21:56:58 -03:00
Natã
a9123e7d66
chore(i18n): add missing pt_BR locale imports for companies, mfa, snooze, webhooks and more (#13844)
## Description

Add missing JSON imports and spread exports for `companies`,
`contentTemplates`, `mfa`, `snooze`, `webhooks`, and `yearInReview` so
these translations are properly loaded in the pt_BR locale. Without
these imports, those sections of the UI were falling back to English for
Brazilian Portuguese users.

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] My changes generate no new warnings

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-03-19 01:27:37 -07:00
Captain
098f7a77b6
chore: Update translations (#13832)
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-03-17 16:02:40 -07:00
Muhsin Keloth
a4c3d3d8c0
feat(widget): Allow widget loading in mobile app WebViews when domain restrictions are set (#13763)
When `allowed_domains` is configured on a web widget inbox, the server
responds with Content-Security-Policy: frame-ancestors <domains>, which
blocks the widget iframe in mobile app WebViews. This happens because
WebViews load content from file:// or null origins, which cannot match
any domain in the frame-ancestors directive.

This adds a per-inbox toggle — "Enable widget in mobile apps" — that
skips the frame-ancestors header when the request has no valid Origin
(i.e., it comes from a mobile WebView). Web browsers with a real origin
still get domain restrictions enforced as usual.

<img width="2330" height="1490" alt="CleanShot 2026-03-11 at 10 13
01@2x"
src="https://github.com/user-attachments/assets/d9326fac-020d-4ce7-9ced-0c185468c8fc"
/>


Fixes
https://linear.app/chatwoot/issue/CW-6560/widget-is-not-loading-from-iosandroid-widgets

How to test

1. Go to Settings → Inboxes → (Web Widget) → Configuration
2. Set allowed_domains to a specific domain (e.g., *.example.com)
3. Try loading the widget in a mobile app WebView — it should be blocked
4. Enable "Enable widget in mobile apps" checkbox
5. Reload the widget in the WebView — it should now load successfully
6. Verify the widget on a website not in the allowed domains list is
still blocked

---------

Co-authored-by: iamsivin <iamsivin@gmail.com>
2026-03-17 14:29:41 +04:00
Sojan Jose
2a90652f05
feat: Add draft status for help center locales (#13768)
This adds a draft status for Help Center locales so teams can prepare
localized content in the dashboard without exposing those locales in the
public portal switcher until they are ready to publish.

Fixes: https://github.com/chatwoot/chatwoot/issues/10412
Closes: https://github.com/chatwoot/chatwoot/issues/10412

## Why

Teams need a way to work on locale-specific Help Center content ahead of
launch. The public portal should only show ready locales, while the
admin dashboard should continue to expose every allowed locale for
ongoing article and category work.

## What this change does

- Adds `draft_locales` to portal config as a subset of `allowed_locales`
- Hides drafted locales from the public portal language switchers while
keeping direct locale URLs working
- Keeps drafted locales fully visible in the admin dashboard for article
and category management
- Adds locale actions to move an existing locale to draft, publish a
drafted locale, and keep the default locale protected from drafting
- Adds a status dropdown when creating a locale so new locales can be
created as `Published` or `Draft`
- Returns each admin locale with a `draft` flag so the locale UI can
reflect the public visibility state

## Validation

- Seed a portal with multiple locales, draft one locale, and confirm the
public portal switcher hides it while `/hc/:slug/:locale` still loads
directly
- In the admin dashboard, confirm drafted locales still appear in the
locale list and remain selectable for articles and categories
- Create a new locale with `Draft` status and confirm it stays out of
the public switcher until published
- Move an existing locale back and forth between `Published` and `Draft`
and confirm the public switcher updates accordingly


## Demo 



https://github.com/user-attachments/assets/ba22dc26-c2e7-463a-b1f5-adf1fda1f9be

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2026-03-17 12:45:54 +04:00
Sojan Jose
270f3c6a80
fix: slim help center search results (#13761)
Fixes help center public article search so query responses stay compact
and locale-scoped. Whitespace-only queries are now treated as empty in
both the portal UI and the server-side search path, and search
suggestions stay aligned with the trimmed query.

Fixes: https://github.com/chatwoot/chatwoot/issues/10402
Closes: https://github.com/chatwoot/chatwoot/issues/10402

## Why

The public help center search endpoint reused the full article
serializer for query responses, which returned much more data than the
search suggestions UI needed. That made responses heavier than necessary
and also surfaced nested portal and category data that made the results
look cross-locale.

Whitespace-only searches could also still reach the backend search path,
and in Enterprise that meant embedding search could be invoked for a
blank query.

## What changed

- return a compact search-specific payload for article query responses
- keep the existing full article serializer for normal article listing
responses
- preserve current-locale search behavior for the portal search flow
- trim whitespace-only search terms on the client so they do not open
suggestions or trigger a request
- reuse the normalized query on the backend so whitespace-only requests
are treated as empty searches in both OSS and Enterprise paths
- pass the trimmed search term into suggestions so highlighting matches
the actual query being sent
- add request and frontend regression coverage for compact payloads,
locale scoping, and whitespace-only search behavior

## Validation

1. Open `/hc/:portal/:locale` in the public help center.
2. Enter only spaces in the search box and confirm suggestions do not
open.
3. Search for a real term and confirm suggestions appear.
4. Verify the results are limited to the active locale.
5. Click a suggestion and confirm it opens the correct article page.
6. Inspect the query response and confirm it returns the compact search
payload instead of the full article serializer.

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2026-03-17 00:46:23 -07:00
Sviat
ef91b8bb42
fix(i18n): improve Ukrainian widget translation (#13819)
## Description

This PR improves the Ukrainian translation for the Chatwoot widget
(`app/javascript/widget/i18n/locale/uk.json`).

Key changes:
- Fixed typo: `Звантажити` → `Завантажити`
- Translated missing English strings
- Improved reply time messages
- Updated day names to match `{day}` usage in `BACK_ON_DAY`
- Improved UX wording in form placeholders
- Fixed typography in `ім’я`
- Improved consistency with other Chatwoot translations

These updates improve readability and correctness of the Ukrainian
widget interface.

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## How Has This Been Tested?

Reviewed the updated translations and verified that:

- Ukrainian translations render correctly
- Reply time messages display properly
- `{day}` values work correctly with the `BACK_ON_DAY` message
- Form placeholders appear correctly
- No untranslated English strings remain

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] My changes generate no new warnings

---------

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-03-16 22:38:08 -07:00
salmonumbrella
a62beffeef
fix(i18n): complete zh_TW locale coverage (#13792)
This updates the Traditional Chinese (`zh_TW`) locale coverage across
Chatwoot so the app no longer falls back to English for missing backend,
dashboard, widget, and survey strings.

## How to test

1. Start Chatwoot locally and switch the UI locale to Traditional
Chinese (`zh_TW`).
2. Walk through the main product areas: dashboard, settings, inbox
management, help center, automations, reports, widget, and survey flows.
3. Confirm the UI surfaces translated Traditional Chinese copy instead
of English fallbacks.
4. Spot-check newly added locale surfaces such as secure password
messaging and snooze UI copy.

---------

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-03-16 18:47:22 -07:00
Denis Petrov
b8f6fe5bb7
feat: Bulgarian locale updates(#13635)
Bulgarian locale updates
2026-03-16 17:47:25 -07:00
Jungu Lee
b866886b55
feat(i18n): complete Korean (ko) translations to 100% coverage (#13583)
Translate all English strings to Korean across 41 frontend locale files
and 2 backend locale files. Add structurally missing keys and translate
existing keys that were left in English.

# Pull Request Template

## Description

Please include a summary of the change and issue(s) fixed. Also, mention
relevant motivation, context, and any dependencies that this change
requires.
Fixes # (issue)

## Type of change

Please delete options that are not relevant.

- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality not to work as expected)
- [ ] This change requires a documentation update

## How Has This Been Tested?

Please describe the tests that you ran to verify your changes. Provide
instructions so we can reproduce. Please also list any relevant details
for your test configuration.


## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream
modules

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-03-16 16:54:26 -07:00
Alexey Krasnoperov
b88236e86e
chore(i18n): update Russian translations (#13405)
# Pull Request Template

## Description

Updated few i18n files to:
1. fix typos / grammar / punctuation
2. translate strings that were still in english
3. add missing keys

## Type of change

- [ ] Bug fix (non-breaking change which fixes an issue)

## How Has This Been Tested?

i18n change, the format remained the same.

## Checklist:

- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules

---------

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-03-16 16:10:16 -07:00
Captain
11ee741716
chore: Update translations (#13227)
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-03-16 15:44:32 -07:00
Daniel Bates
03719cede0
fix: Correct reversed message status indicators for API channel (#13594)
## Description

Fixes the reversed message delivery status indicators for the API
channel. The API inbox was grouped with the web widget inbox in the
`isDelivered` computed property, causing both to treat a `sent` status
as `delivered`. Since the API channel provides real
`sent`/`delivered`/`read` status values from external systems (unlike
the web widget which has no separate delivery confirmation), the API
inbox needs its own handling.

**Before this fix:**
- Status `sent` (0) → incorrectly showed delivered checkmarks
- Status `delivered` (1) → incorrectly showed "Sending" spinner

**After this fix:**
- Status `sent` → correctly shows sent indicator (single checkmark)
- Status `delivered` → correctly shows delivered indicator (double
checkmarks)
- Status `read` → unchanged (already worked correctly)

The web widget inbox behavior is unchanged — it still treats `sent` as
`delivered` since it lacks a separate delivery confirmation mechanism.

Fixes #13576

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## How Has This Been Tested?

Verified by code review that the computed properties now correctly map
API channel message statuses:
- `isSent` returns `true` when `status === 'sent'` for API inbox
- `isDelivered` returns `true` when `status === 'delivered'` for API
inbox
- `isRead` unchanged — already checks `status === 'read'` for API inbox
- Web widget inbox logic is unchanged

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [x] My changes generate no new warnings

*This PR was created with the assistance of Claude Opus 4.6 by
Anthropic. Happy to make any adjustments! Reviewed and submitted by a
human.*

Co-authored-by: Your Name <your-email@example.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2026-03-16 13:21:18 +04:00
Tanmay Deep Sharma
a452ce9e84
feat(whatsapp): add webhook registration and status endpoints (#13551)
## Description

Adds webhook configuration management for WhatsApp Cloud API channels,
allowing administrators to check webhook status and register webhooks
directly from Chatwoot without accessing Meta Business Manager.

## Type of change

- [ ] New feature (non-breaking change which adds functionality)


## Screenshots

<img width="1130" height="676" alt="Screenshot 2026-03-05 at 7 04 18 PM"
src="https://github.com/user-attachments/assets/f5dcd9dd-8827-42c5-a52b-1024012703c2"
/>
<img width="1101" height="651" alt="Screenshot 2026-03-05 at 7 04 29 PM"
src="https://github.com/user-attachments/assets/e0bd59f9-2a90-4f24-87c0-b79f21e721ee"
/>



## Checklist:

- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2026-03-16 12:48:16 +05:30
Shivam Mishra
73a90f2841
feat: update bunny video support in HC (#13815)
Bunny Video has added a new URL player.mediadelivery.net, this PR adds
support for the new URL
2026-03-16 11:04:27 +05:30
Aakash Bakhle
b103747584
fix: skip Enter key submission during IME composition in AI inputs (#13779)
# Pull Request Template

## Description

CJK language users (Chinese, Japanese, Korean, etc.) use IME where Enter
confirms character selection. AI input components were intercepting
Enter unconditionally, making them unusable for IME users.

Add `event.isComposing` check to CopilotEditor, CopilotInput, and
AssistantPlayground so Enter during active IME composition is ignored.


## Type of change

Please delete options that are not relevant.

- [x] Bug fix (non-breaking change which fixes an issue)

## How Has This Been Tested?

Please describe the tests that you ran to verify your changes. Provide
instructions so we can reproduce. Please also list any relevant details
for your test configuration.

Before:

Add a Japenese keyboard, then go to AI follow-ups, type some word,
selecting it with enter submits the follow up. So CJK users cannot use
follow-ups.



https://github.com/user-attachments/assets/53517432-d97b-47fc-a802-81675e31d5c9



After:

Type a word, press enter to choose it, press enter again to unselect it
and enter again to send


https://github.com/user-attachments/assets/6c2a420b-7ee6-4c71-82a6-d9f1d7bbf31a



## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream
modules

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 10:26:51 +05:30
Gabriel Jablonski
05d47931be
feat: add kanban feature promotion with upgrade banner (#235)
* feat: add kanban feature promotion with upgrade banner

Add a Kanban sidebar item visible to all users that shows a locked
feature promotion page. Super admins see an upgrade button linking to
fazer.ai, while non-admins see a message to contact their administrator.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: allow custom_role users to access kanban promotion page

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 15:31:53 -03:00
gabrieljablonski
477e0ad445 chore: fix fazer.ai render 2026-03-11 19:12:04 -03:00
Petterson
6e46be36c8
fix: Add fix to only allow confirmed agents to used in Agent Assingments at Macros/Automations (#13225)
# Pull Request Template

## Description

Unconfirmed agents (pending email verification) were incorrectly
appearing in the "assign agent" dropdown for macros and automations.
This fix filters out unconfirmed agents from these dropdowns and adds
backend validation to prevent assignment of unconfirmed agents.

Fixes #13223

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## How Has This Been Tested?

**Backend tests:**
```bash
docker compose run --rm rails bundle exec rspec spec/services/action_service_spec.rb
```
- Added tests for confirmed agent assignment (should succeed)
- Added tests for unconfirmed agent assignment (should be skipped)

**Frontend tests:**
```bash
docker compose run --rm rails pnpm test app/javascript/dashboard/composables/spec/useMacros.spec.js
```
- Updated mocks to use `getVerifiedAgents` getter

**Manual testing:**
1. Create an unconfirmed agent via platform
2. Navigate to Settings → Macros → New Macro → Add "Assign Agent" action
3. Verify unconfirmed agent does NOT appear in dropdown
4. Navigate to Settings → Automations → New Automation → Add "Assign
Agent" action
5. Verify unconfirmed agent does NOT appear in dropdown

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules

---------

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-03-11 02:01:53 -07:00
Aakash Bakhle
87f5af4caa
fix: playground captain v2 scenarios (#13747)
# Pull Request Template

## Description

Playground now uses v2. It was only wired to use v1. Traces get `source:
playground` on langfuse when playground has been used.

## Type of change

- [x] New feature (non-breaking change which adds functionality)

## How Has This Been Tested?

Please describe the tests that you ran to verify your changes. Provide
instructions so we can reproduce. Please also list any relevant details
for your test configuration.

locally and specs
<img width="1806" height="1276" alt="image"
src="https://github.com/user-attachments/assets/41ef4eb3-52b1-4b8e-9a4f-e8510c90cb39"
/>


## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream
modules
2026-03-11 14:05:16 +05:30
Sivin Varghese
a9cabad529
chore: Hide reply-to when copilot is active (#13749) 2026-03-11 11:30:30 +05:30
Shivam Mishra
8d9dd99012
fix: scenario label (#13746) 2026-03-10 18:32:44 +05:30
gabrieljablonski
a4ff73d496 feat: add validation and error messages for custom HTML fields in portal settings 2026-03-09 11:48:53 -03:00
Gabriel Jablonski
eaac65c973
feat: add custom HTML fields for portals (#233) 2026-03-09 11:47:41 -03:00
Tanmay Deep Sharma
11826e2a21
perf: reduce presence update frequency and fix background tab throttling (#13726)
## Description
Reduces the frequency of update_presence WebSocket calls from the live
chat widget and fixes agents appearing offline when the dashboard is in
a background tab.

## Fixes # (issue)
https://github.com/chatwoot/chatwoot/issues/13720

## Type of change

- [ ] Bug fix (non-breaking change which fixes an issue)


## Checklist:

- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
2026-03-09 18:23:44 +05:30
Sivin Varghese
f4e6aa1bd2
fix: ProseMirror prompt modal UI issue (#13722) 2026-03-09 16:51:49 +05:30
Muhsin Keloth
939471cb3b
fix: Prevent duplicate conversations in conversation list (#13713)
Agents using API channel inboxes (e.g., WhatsApp Automate) reported
seeing the same conversation appear twice in their conversation list —
one showing the last message preview and the other showing "No
Messages". Backend investigation confirmed no duplicate conversations
exist in the database, making this purely a frontend issue.

The root cause is a race condition in WebSocket event delivery. When a
conversation is created via the API with auto-assignment, the backend
enqueues multiple ActionCable broadcast jobs (`conversation.created`,
`assignee.changed`, `team.changed`) within milliseconds of each other.
In production with multi-threaded Sidekiq workers, these events can
arrive at the frontend out of order. If `assignee.changed` arrives
before `conversation.created`, the `UPDATE_CONVERSATION` mutation pushes
the conversation into the store (since it doesn't exist yet), and then
`ADD_CONVERSATION` blindly pushes it again — resulting in a duplicate
entry.

The fix adds a uniqueness check in the `ADD_CONVERSATION` mutation to
skip the push if a conversation with the same ID already exists in the
store, matching the dedup pattern already used by
`SET_ALL_CONVERSATION`.
2026-03-06 14:07:02 +04:00
Sivin Varghese
88587b1ccb
feat: Add natural language date parser for snooze functionality (#13587)
# Pull Request Template

## Description

This PR introduces a custom, lightweight natural-language date parser
(dependency-free except for date-fns) to power snooze actions via the
command bar (e.g., “Remind me tomorrow at 6am”). It also adds support
for multi-language searches.



<details>
  <summary>Supported Formats</summary>

## Snooze Date Parser — Supported Input Formats


## 1. Durations

Specify an amount of time from now.

### Basic

- `5 minutes` · `2 hours` · `3 days` · `1 week` · `6 months` · `ten
year`
- `in 2 hours` · `in 30 minutes` · `in a week` · `in a month`
- `5 minutes from now` · `a week from now` · `two weeks from now`

### Half / fractional

- `half hour` · `half day` · `half week` · `half month`
- `in half a day` · `in half an hour` · `in half a week`
- `one and a half hours` · `in one and a half hours`
- `1.5 hours` · `2.5 days`

### Compound

- `1 hour and 30 minutes` · `2 hours and 15 minutes`
- `2 days at 3pm` · `1 week at 9am`

### Shorthand (no spaces)

- `2h` · `30m` · `1h30m` · `2h15m`
- `1h30minutes` · `2hr15min` · `1hour30min`

### Informal quantities

- `couple hours` · `a couple of days` · `in a couple hours`
- `a few minutes` · `in a few hours` · `in a few days`
- `fortnight` · `in a fortnight` _(= 2 weeks)_

### Trailing "later"

- `2 days later` · `a week later` · `month later`

### Prefix words (`after` / `within`)

- `after 2 hours` · `after 3 days` · `after ten year`
- `within a week` · `within 2 hours`

### Recognised word-numbers

`a` (1) · `an` (1) · `one` – `twenty` · `thirty` · `forty` · `fifty` ·
`sixty` · `ninety` · `half` (0.5) · `couple` (2) · `few` (3)

---

## 2. Relative Days

- `today` · `tonight` · `tomorrow`
- `tomorrow morning` · `tomorrow afternoon` · `tomorrow evening` ·
`tomorrow night`
- `tomorrow at 3pm` · `tomorrow 9` · `tonight at 8` · `tonight at 10pm`
- `tomorrow same time` · `same time tomorrow`
- `day after tomorrow` · `the day after tomorrow` · `day after tomorrow
at 2pm`
- `later today` · `later this afternoon` · `later this evening`

---

## 3. Weekdays

- `monday` · `friday` · `wed` · `thu`
- `friday at 3pm` · `monday 9am` · `wednesday 14:30`
- `monday morning` · `friday afternoon` · `wednesday evening`
- `monday morning 6` · `friday evening 7`
- `this friday` · `upcoming monday` · `coming friday`
- `same time friday` · `same time wednesday`

---

## 4. "Next" Patterns

- `next hour` · `next week` · `next month` · `next year`
- `next week at 2pm` · `next month at 9am`
- `next monday` · `next friday` · `next friday at 3pm`
- `next monday morning` · `next friday evening`
- `monday of next week` · `next week monday`
- `next january` · `next december`
- `next business day` · `next working day`

---

## 5. Time of Day

- `morning` · `afternoon` · `evening` · `night` · `noon` · `midnight`
- `this morning` · `this afternoon` · `this evening`
- `early morning` · `late evening` · `late night`
- `morning at 8am` · `evening 6pm` · `afternoon 2pm`
- `eod` · `end of day` · `end of the day`

---

## 6. Standalone Time

- **12-hour:** `3pm` · `9am` · `at 3pm` · `at 9:30am`
- **24-hour:** `14:30` · `at 14:30`

---

## 7. Named Dates (Month + Day)

- `jan 15` · `january 15` · `march 20` · `dec 25`
- `jan 1st` · `march 3rd` · `april 2nd` · `december 31st`
- `15 march` · `25 dec` _(reversed order)_
- `jan 15 2025` · `dec 25 2025` · `march 20 next year`
- `jan 15 at 2pm` · `march 5 at 2pm`
- `december 2025` · `january 2024` _(month + year only)_

---

## 8. Month + Ordinal Patterns

Target a specific week or day within a month.

### Week of month

- `april first week` · `july 2nd week` · `feb 3rd week`
- `first week of april` · `2nd week of july`

### Day of month

- `april first day` · `march second day` · `march 5th day`
- `third day of march` · `5th day of jan at 2pm`

### Supported ordinals

- **Digit:** `1st` `2nd` `3rd` `4th` `5th` … (up to 31 for days, 5 for
weeks)
- **Word:** `first` `second` `third` `fourth` `fifth` `sixth` `seventh`
`eighth` `ninth` `tenth`

---

## 9. Formal / Numeric Dates

- **ISO:** `2025-01-15`
- **Slash (M/D/Y):** `01/15/2025`
- **Dash (D-M-Y):** `15-01-2025`
- **Dot (D.M.Y):** `15.01.2025`
- Any of the above **+ time:** `2025-01-15 at 3pm`

---

## 10. Special Phrases

- `this weekend` · `weekend` · `next weekend`
- `end of week` · `end of month`
- `end of next week` · `end of next month`
- `beginning of next week` · `start of next week`
- `beginning of next month`

---

## 11. Noise / Filler Stripping

The parser silently removes conversational prefixes so all of these work
exactly the same as the bare expression:

```
snooze for 2 hours          →  2 hours
remind me tomorrow          →  tomorrow
please snooze until friday  →  friday
can you set a reminder for next week  →  next week
schedule this for jan 15    →  jan 15
postpone to next monday     →  next monday
defer for 2 days            →  2 days
delay it by 1 hour          →  1 hour
```

### Recognised filler verbs / prefixes

`snooze` · `remind` · `remind me` · `set a reminder` · `add a reminder`
·
`schedule` · `postpone` · `defer` · `delay` · `push`

### Recognised prepositions (stripped)

`on` · `to` · `for` · `at` · `until` · `till` · `by` · `from` · `after`
· `within`

### Typo corrections

`tommorow` / `tommorrow` → `tomorrow` · `nxt` → `next`

---

## 12. Multi-Language Support

The parser supports localised input via translations in `snooze.json`.

### Translatable token categories

- **Units:** minute, hour, day, week, month, year _(singular + plural)_
- **Relative days:** tomorrow, day after tomorrow, next week / month,
this / next weekend
- **Time of day:** morning, afternoon, evening, night, noon, midnight
- **Word numbers:** one through ten, twelve, fifteen, twenty, thirty
- **Ordinals:** first through fifth
- **Structural words:** at, in, of, after, week, day, from now, next
year
- **Meridiem:** am, pm

### Auto-detected from locale

Weekday names and month names are resolved automatically via
`Intl.DateTimeFormat` for the user's locale — no manual translation
needed.

</details>

## Type of change

- [x] New feature (non-breaking change which adds functionality)

## How Has This Been Tested?

**Screenshots**
<img width="974" height="530" alt="image"
src="https://github.com/user-attachments/assets/c690d328-a0df-41d2-b531-2b4e6ce6b5fd"
/>
<img width="974" height="530" alt="image"
src="https://github.com/user-attachments/assets/fa881acc-4fed-4ba3-9166-58bd953bcb26"
/>
<img width="974" height="530" alt="image"
src="https://github.com/user-attachments/assets/4d9a224b-641c-409c-a7ce-3dec2b5355e2"
/>
<img width="974" height="530" alt="image"
src="https://github.com/user-attachments/assets/465b9835-d82c-4bc7-a2ae-94976ada2d3b"
/>
<img width="974" height="530" alt="image"
src="https://github.com/user-attachments/assets/839fe8fc-8943-4b66-83ca-5c61c95f24d8"
/>
<img width="974" height="530" alt="image"
src="https://github.com/user-attachments/assets/3a9a54f2-7669-40f2-b098-a3f5c183526d"
/>
<img width="974" height="530" alt="image"
src="https://github.com/user-attachments/assets/7791ab2b-c763-49a9-90a0-e91b0d8f0a26"
/>
<img width="974" height="530" alt="image"
src="https://github.com/user-attachments/assets/4689390c-0e7f-48ae-acc7-d8e28695452f"
/>
<img width="974" height="530" alt="image"
src="https://github.com/user-attachments/assets/d0aa5217-d0e1-4f41-b663-72888d028a3a"
/>
<img width="974" height="530" alt="image"
src="https://github.com/user-attachments/assets/4fa9ff5b-a874-43d5-812f-6abe1a95a5ac"
/>
<img width="974" height="530" alt="image"
src="https://github.com/user-attachments/assets/2c8199a6-f872-46af-986f-bdf8597248f5"
/>
<img width="974" height="530" alt="image"
src="https://github.com/user-attachments/assets/5bd9effc-7518-4f96-b2f2-7c547f32f500"
/>




## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
2026-03-06 12:20:22 +04:00
Sojan Jose
42a244369d
feat(help-center): enable drag-and-drop category reordering (#13706) 2026-03-05 12:53:38 +05:30
Sivin Varghese
f24e7eb231
fix: Missing required prop warning in account settings page (#13711)
# Pull Request Template

## Description

This PR fixes the console warning in development: `[Vue warn]: Missing
required prop: "name"` on the account settings page.


## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## How Has This Been Tested?

**Screenshot**
<img width="599" height="1036" alt="image"
src="https://github.com/user-attachments/assets/b0b45854-4cfb-4fe7-ab14-c42a65c523df"
/>



## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
2026-03-04 19:58:47 +04:00
Gabriel Jablonski
195713cbfe
chore: general improvements (#232) 2026-03-03 14:08:56 -03:00
Muhsin Keloth
a1b98a253c
fix(ui): Show delivered state for Instagram external echo messages (#13700)
Instagram external echo messages were being saved with status:
delivered, but the message meta UI did not treat Instagram as a channel
eligible for delivered-state rendering. As a result, these messages fell
back to progress and showed as “Sending”. This change updates the
message status mapping in the new message UI to include Instagram in the
delivered-state condition.
2026-03-03 15:16:53 +04:00
Sivin Varghese
89da4a2292
feat: compose form improvements (#13668) 2026-03-02 18:27:51 +05:30
Gabriel Jablonski
56c5609ca0
feat: add per-inbox signature management (#226)
* feat: add per-inbox signature management

- Introduced `InboxSignature` model to manage signatures specific to each inbox.
- Added API endpoints for fetching, creating, updating, and deleting inbox signatures.
- Updated UI components to support inbox-specific signatures, including overrides for signature position and separator.
- Implemented a new composable `useInboxSignatures` for managing inbox signatures in the frontend.
- Enhanced existing components to utilize inbox signatures, including the reply box and message signature settings.
- Added tests for the new inbox signatures functionality, ensuring proper behavior of the API and model validations.
- Updated translations for new UI elements related to inbox signatures.

* feat: implement inbox access validation and add related tests

* feat: enhance inbox signatures fetching and management logic
2026-02-26 19:53:03 -03:00
Gabriel Jablonski
21007bd20b
feat: add show author option to portal settings and update related views (#225)
* feat: add show author option to portal settings and update related views

* fix: update portal reference to use local variable for show author condition

* feat: enhance show_author handling in portal config and add related tests
2026-02-26 14:32:52 -03:00
Tanmay Deep Sharma
9ca03c1af3
chore: make all the deprecated feature flag reclaimable (#13646)
## Docs

https://www.notion.so/chatwoot/Redeeming-a-depreciated-feature-flag-313a5f274c9280f381cdd811eab42019?source=copy_link

## Description
Marks 8 unused feature flags as deprecated: true in features.yml,
freeing their bit slots for future reuse.
Removes dead code references from JS constants, help URLs, and
enterprise billing config.

## Type of change

- [ ] Bug fix (non-breaking change which fixes an issue)

## How Has This Been Tested?

- Simulated the "claim a slot" workflow 

## Checklist:

- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
2026-02-26 18:01:13 +05:30
Shivam Mishra
c218eff5ec
feat: add per-webhook secret with backfill migration (#13573) 2026-02-26 17:26:12 +05:30
Muhsin Keloth
6b3f1114fd
fix(slack): Show correct sender name and avatar for Slack replies (#13624) 2026-02-26 16:15:15 +05:30
Sivin Varghese
109b43aadb
chore: Disable API channel reply editor outside 24h window (#13664) 2026-02-26 16:05:05 +05:30