1 file 8

Cut message-container into pieces

Leave a thin route coordinator. Move inbox, thread, composer, and automated messages out in separate PRs. Do not rewrite the file.

7 Oct 2026, hotfix/release-0.0.23/messages-revamp. Phase 1 already stopped history from waiting on permissions. This page is the split. Notes that say "speed first, then cut" are dated.

The pile

4,746
TypeScript. Inbox cursors sit next to tip popups and a waveform canvas. More than a hundred methods share one class.
3,180
Template. Left pane, header, thread, composer, automated tab, share modals. One @for paints every bubble type.
1,228
SCSS. Extract styles with the component that owns the markup. Do not leave a leftover mega sheet.
FileTSHTMLSCSS
message-container4,7463,1801,228
new-message-model580427371
image-audo-message526384153
automated-message-modal3982211
edit-participants-modal243249212
message-price-model646732
message-price-model-v2593314

Already out, and still not enough

The seven siblings handle new-message, mailing-list participants, two price modals, automated-message media, and the automated-message form. The container still owns about 95% of the behavior. message-price-model and message-price-model-v2 are the same smell at modal scale. Pick one later. Do not add a third.

No messages.module.ts. The lazy route already points at a standalone MessageContainerComponent with canDeactivate and a route-scoped AudioRecordingService. New files follow that shape.

What I would cut

Visible tree

app-message-container          route, selected id
  app-inbox-panel              search, tabs, list
    app-inbox-row              one DM or mailing list
  app-conversation-header      avatar, online, block
  app-message-thread           history, scroll up
    app-message-bubble         later, not first
  app-message-composer         text, emoji, tip, send
    app-paid-attachment-strip  CDK paid / free lists
    app-composer-audio         record + waveform
  app-automated-messages-tab   table + empty state

Children receive data and emit intents. They do not call get-chatGroups-and-users. Share modals stay lazy. They already live at the bottom of the template. Keep opening them with await import(...) on click.

Services, only when a method cluster has one job

File under messages/services/Owns
messages-inbox.service.tsCursor, search debounce, badge counts, loadInboxPage
messages-thread.service.tsHistory cursor, merge live + fetched, dmValidations
messages-send.service.tsQueue, S3, sendMessageApiCall, hasPendingSend$
messages-realtime.facade.tsOne ChatService.receiveMessage subscription
messages-payment-flow.service.tsTip, pay-per-DM, guaranteed reply, make-payment
messages-wls.service.tsOwner chat target, allow-DM flag. Last.

ChatService stays the socket pipe. A MessagesStore PR that copies fields off the class and leaves the methods behind is a second pile.

What stays in the container

  • The lazy route target and userProfileData() boot.
  • Which conversation is open. Inbox emits. Thread and composer read it.
  • canDeactivate, reading hasPendingSend$ from the send service once that exists. Until then it still reads messsageQueue.
  • Header and footer visibility through HeaderFooterStateService.
  • White-label layout classes on the shell, until the WLS service exists.

Leave these alone in the first cuts

NgRx is already thin. The container only selects the profile. Do not spread the store into leaf components.

Do not convert the leftover container to signals in the same PR as an extract. New files may use input() and output(). The old class keeps working.

Order of work

PR 1, no socket
Inbox service + panel
  • loadInboxPage, search, scroll
  • Container still opens the thread
PR 2, already a tab
Automated messages
  • Table, empty state, CRUD hooks
  • Modal stays where it is
PR 3, presentational
Conversation header
  • Avatar, online, mailing actions
  • Block and report still open existing dialogs
PR 4, biggest HTML
Thread as a list
  • @Input messageList, load-older output
  • No bubble split yet
PR 5, CDK stays together
Paid attachment strip
  • Connected paid and free lists
  • Mobile bridge file input
PR 6, canDeactivate
Send service + composer
  • Queue, S3, tip popup, emoji
  • Audio recorder moves with it
PR 7, one subscribe
Realtime facade
  • Delete the extra receiveMessage sub
  • Inbox and thread both listen here
PR 8, after DM works
Payments, then WLS
  • Tip and pay-per-DM next
  • White-label last. It branches every pane.

Inbox first because it has no socket and no upload. Automated next because it is already a second tab that the DM pane does not need. Thread as a dumb list, then composer. A bubble component folder is polish after the thread file exists. MESSAGES_ROUTES does not change path.

God methods I would not copy

onBroadcast 1167-1326inbox + new-message modal Four recipient shapes in one method. Featured, mailing list, single user, followers. Split by the caller, not by another else if.
sendMessageApiCall 2460-2578send service Pay-and-tip, pay, tip, consume credits, guaranteed reply, then socket send. That is a payment flow calling chat as a side effect.
uploadPriceItemsGeneric 2260-2370send service Image blur, share links, portfolio, events, XHR, restricted-content modal. Keep the matrix in one service. Do not spread it across bubble components.
getUserChatMessage 3359-3488thread service Cancels, permissions, live-merge twice on the success path, cursor paging. The thread component should call one method and render the list it gets back.
Smell
ngAfterViewChecked at 2718 calls detectChanges() on the whole tree, every cycle.
Cause
The template is too large for Angular to notice what changed, so the class forces a second pass.
Fix
Delete it after inbox, thread, and composer own their own change detection. Do not delete it as the first commit. The giant template still needs the lie until the children exist.
Smell
ChatService.receiveMessage is subscribed in ngAfterViewInit and again inside sliceMessageFromMessageQueue.
Cause
A send-path patch that did not trust the view-init subscription.
Fix
One facade. The second subscribe is PR 7, after composer moves, so you can see both call sites without the send method still living in the container.

Coupling that will bite

RiskWhereWhat to do
Send queue messsageQueue, canDeactivate at 4254 Expose hasPendingSend$ from the send service. The guard stays on the route.
CDK drag-drop Paid and free lists, todoList / doneList Keep both lists in one component. Splitting them across templates breaks the connected lists.
Infinite scroll Inbox down, history up, shared CHAT_SCROLL Pass the constant in. Do not share one handler.
Audio Route providers: [AudioRecordingService] Move the provider onto the composer or the audio child when that file exists.
Direct DOM document.getElementById in setMessageQueue, plus declare var $ Composer uses viewChild for #editable. Drop jQuery when that method moves.
Seven Injector.get calls Constructor 453-457, then more Constructor DI on the service that actually needs the dependency. Stop parking services on public fields.
Payment redirect dashboard/make-payment plus CommonService.subscriptionPaymentData Payment-flow service. Composer emits "payment required".
White-label showUsersList, auto-open owner chat, exclusive empty state Last. Every earlier PR should keep the current WLS branches compiling, even if they still live on the shell.

After the first two PRs

src/app/modules/messages/- components/message-container/*.ts        4746+ components/message-container/*.ts        ~3900+ components/inbox-panel/*+ components/automated-messages-tab/*+ services/messages-inbox.service.ts  components/new-message-model/*           unchanged  messages.routes.ts                       still MessageContainerComponent

What "working" means after each cut

  • Inbox still pages, searches, and shows Priority / General / Bulk badges.
  • Opening a row still fills the thread. That handler stays on the container until the thread PR.
  • Automated tab still lists, creates, edits, and deletes. The DM pane does not import app-table.
  • Leaving mid-send still blocks navigation.
  • White-label owner and fan layouts still compile. Visual check on both.

Done when

  • Container TypeScript under 1,500 lines. Template under 400.
  • One receiveMessage subscription.
  • No ngAfterViewChecked detectChanges().
  • canDeactivate still blocks on a non-empty send queue.
  • Inbox, thread, composer, and automated tab each have a folder under messages/components/.

I would not call it done if

  • A MessagesStore exists and the container still has sendMessageApiCall.
  • Bubble components landed before the thread parent.
  • Signals or zoneless leaked into the leftover container in the same PR as an extract.
  • White-label was "cleaned up" in the inbox PR and fan layout broke.

Open

QuestionDefault if nobody answers
Keep ngx-bootstrap tabset for Messages vs Automated, or give automated its own route? Keep the tabset on the shell. A second route is a product change.
AudioRecordingService on the route or on the audio child? Move it with the composer in PR 6.
Which price modal survives? The one the composer still opens. Delete the unused file in a later cleanup, not in PR 1.
Do we rename image-audo-message while we are here? No. Typo rename is its own PR so git blame stays readable.

Folder names above are the ones I would type. If a later extract wants app-inbox instead of app-inbox-panel, that is fine. The order is the decision.