Break up the messages page and make it faster

Ship the loading fix first. Then split the page around inbox, thread, composer, and automated messages. Use Angular 21 patterns in each extracted component, with separate changes for app-wide migrations.

Recommendation by sol-6-1, 1 Oct 2026. Audited frontend b4eecd4a60 and the current source against the Opus speed investigation. This document proposes the work. It does not implement or benchmark it.

Why I would start with the loading fix

MessageContainerComponent has 4,283 lines of TypeScript, 3,118 lines of HTML, and 1,218 lines of SCSS. Splitting it will make changes easier to review. File count alone will not remove a network wait or keep an emoji library out of the first download.

The speed report identifies a smaller change that can ship before the extraction. I checked these findings in the current source:

  • getUserEmail() waits 1,000 ms before validation, even when the selected participant is available.
  • getUserChatMessage() and checkNewUserValidation() both emit to userValidationCancel$. Parallelizing them without changing cancellation would cancel one request.
  • msgrightLoader hides history and the composer together. The inbox request also clears it in its completion handler.
  • scrollToBottom() waits another 500 ms. ngAfterViewChecked() calls detectChanges() on every view check.
  • The container eagerly imports the picker, audio player, table, and share dialogs. receiveMessage() also refetches the inbox.

The first change should establish a reliable selected-thread flow. That gives the extraction a clearer contract. A full component rewrite first would leave users waiting while we move the same problems between files.

The report's timing and bundle estimates are hypotheses from an earlier build. Its author had no authenticated timing session. I confirmed the source patterns above, not the proposed 0.6 s load time or 450 KB saving.

The order I recommend

OrderChangeReady to move on when
1. Establish a baselineCapture production route-to-history and route-to-composer timings, request order, and loaded chunks. Record the existing selection and payment behavior.We can repeat the same scenarios and compare changes.
2. Fix thread loadingSeparate history and permission cancellation, loading, and errors. Use the selected participant instead of the 1 s timer. Start both requests together. Render authorized history independently.Fast A-to-B switching never displays A in B. Permission failure leaves actions disabled and history readable.
3. Extract optional UIMove automated messages into its own panel. Load its table and editor on tab selection. Load dialogs on demand. Keep behavior changes limited to loading.A normal thread visit does not download automated-message or unused dialog code.
4. Extract inbox and threadMove list selection, timeline, header, and rendering into their owners. Sort once, track stable IDs, and replace repeated template filtering. Fix view updates before removing the repeated detection hook.Unread counts, default selection, mobile layout, and existing message types still behave correctly.
5. Extract the composerMove draft, attachments, recording, tip UI, and send coordination. Defer the picker and recorder. Preserve payment rules and the current send queue.Paid DM, free credits, guaranteed reply, upload failure, and optimistic sends pass regression checks.
6. Address remaining costsAdd bounded caching, pagination, websocket deltas, and backend changes where measurements justify them.Each change improves its measured cost without losing messages or corrupting counts.

These are review boundaries, not a requirement for six large PRs. Split a stage further when it mixes unrelated risks. Add the history and inbox skeletons when their loading states become separate.

One coordinated effort, sequential frontend changes. Research, backend API discussion, and baseline collection can overlap. Two frontend branches editing the same container will create conflicts and make failures harder to attribute. Work in parallel once the extracted components have agreed inputs and outputs.

The proposed final component tree

Keep MessageContainerComponent as the route entry. It owns page layout, feature tabs, route integration, and the existing navigation guard. The children below are proposed components unless marked as existing. Shared buttons, tabs, skeletons, and icons are omitted from the tree.

  • MessageContainerComponentPage shell and route coordination
    • MessagesInboxComponentSearch, Priority / General / Bulk tabs, loading and empty states
      • ConversationRowComponentDirect and mailing-list rows, preview, unread count, row actions
    • MessageThreadComponentSelected conversation, independent history and permission states
      • MessageThreadHeaderComponentParticipant or group, presence, badges, block/report, mobile back
      • MessageTimelineComponentHistory, day grouping, scroll position, older-page state
        • MessageItemComponentOne incoming or outgoing message, timestamp, optimistic status
          • MessageMediaComponentImages, audio, video and files, with playback code loaded when needed
          • PaidMessageContentComponentLocked content, free previews, price, unlock request
          • ShareDetialsPreviewComponentExisting merchandise, portfolio and event preview component
      • MessageComposerComponentDraft UI, send state, permission prompts, composing actions
        • MessageEditorComponentText, cursor, keyboard handling, insert first name
        • MessageAttachmentTrayComponentPaid and free attachment lists, previews, reorder and remove
        • MessageTipPanelComponentTip amount, currency and guaranteed-reply UI
        • MessageEmojiPickerComponentLoad when opened
        • MessageAudioRecorderComponentLoad when opened, with waveform and recording controls
    • AutomatedMessagesPanelComponentLoad on tab selection, own fetch/edit/delete state
      • TableComponentExisting shared table
The picker, recorder, automation panel, and heavy players are code-loading boundaries. Inbox, thread header, and readable history stay on the initial path.

Dialogs sit outside this permanent tree. A lightweight MessagesDialogService loads the existing new-message, participants, price, share, image-preview, and video dialogs when requested. Extract the container's inline delete and tip/guaranteed-reply dialogs into dedicated components based on AlertModalComponent.

Do not make a component for every icon or conditional branch. The useful boundary is a part with its own behavior, a repeated renderer, or an expensive dependency. The first extraction can be coarser than this tree.

There is already an ImageAudioMessageComponent used by the automated-message editor. It duplicates attachment and recording behavior and owns mutable state. Keep it working during the first extraction. Share the new attachment and recording pieces after both consumers have a compatible draft contract.

Moving the template is only half the work

The container should stop owning API calls, upload steps, payment branches, and message formatting. Copying all 4,283 TS lines into a single service would leave the maintenance problem intact.

OwnerResponsibility
MessagesFacadePage-session coordination, route targets, white-label default target, and a small read-only page model. Delegate domain work below.
MessagesInboxStoreInbox data, search, classification, sorting, unread totals, selection rules and row updates.
MessageThreadStoreHistory and permission request state for a selection, retries, pagination, deduplication and optimistic-message reconciliation.
MessageComposerStoreDraft and attachment state, upload/send queue and busy state. Keep destination and permission context attached to each queued send.
MessageActionsServicePaid DM credit consumption, tips, guaranteed replies, unlocks and existing payment flows. Reuse current application payment services.
MessagesApiServiceTyped adapters over existing endpoints. GET uses ApiCacheService, initially ALWAYS_FRESH. Existing history and validation POSTs keep using ApiCallService.
MessagesRealtimeServiceOne messages-page subscription to existing ChatService. Map events to stores. Rows and message items never subscribe separately.

Keep page-session state with the container and draft state with its owning composer. Do not assume a route provider is destroyed on every navigation; define session reset and subscription cleanup explicitly. Stateless adapters can be root singletons. A later root cache should retain only bounded, intentional data and clear it on account or tenant changes.

Components receive typed, read-only models and emit actions such as select, retry, send, remove attachment, and unlock. Do not pass the parent component to a child or let children mutate its arrays. Keep temporary forwarding methods while extracting, then remove them.

The thread selection includes groupId, participant ID, conversation kind, and account/tenant context. History and permission streams independently cancel old selections. Late responses and their cleanup must check the selection generation before updating state. A shared forkJoin would wait for both responses and hide the benefit of early history.

Normalize API responses once into message and conversation models. Give an optimistic send a stable render ID that survives the server response; preserve the existing uinqueId matching until that adapter is tested. Replacing an optimistic ID with a server ID can otherwise recreate the row and interrupt media.

Where I would adjust the speed report

Accept the loading changes, keep selection rules explicit

Start history and permissions together when the participant is known. Handle direct links, new groups, broadcasts, and white-label owner targeting separately. Clear the previous thread's permission state immediately. Unknown or failed validation must never fall through to free sending.

Pair code splitting with extraction

A smaller component remains eager if the shell still imports it eagerly. Use deferred standalone wrappers for the picker, players, and automation panel, and dynamic imports for dialogs. A child class referenced outside its defer block or in a component query can remain eager. Making a query optional does not fix that reference. Verify the actual chunks. Angular documents these defer conditions.

Move scrolling to the timeline, then remove timing guesses

The timeline owns scrolling. Use a browser render callback such as afterNextRender to wait for the view. Give media reserved dimensions and account for later media sizing. Keep the reader's position when older history arrives or a new message arrives while they read above the bottom. Angular's render callback guidance explains when callbacks run and why SSR needs separate handling.

Defer cache and backend work until the frontend contracts settle

The proposed paginated inbox, thread bootstrap, and websocket row deltas are sensible API discussions. They do not block the first loading fix or component split. Keep the full-inbox fallback until event payloads can maintain classification and unread totals. Decide on virtualization after profiling large inboxes. Variable-height message media makes timeline virtualization a separate task.

Early prefetch must not pull the whole messages dependency tree into the app shell. Keep any prefetch adapter small and browser-only. Private cache keys need account, tenant, permission context, tab, search and conversation identity. The current GET cache keys come from the endpoint, so do not assume they provide that isolation.

Angular 21 source changes belong in the extraction

package.json already targets Angular 21.2.x and TypeScript 5.9. The messages route already loads lazily, the container is standalone, its template uses @if and @for, and its queries use viewChild(). A new package-upgrade phase is unnecessary for this checkout. Most remaining work is state ownership and update behavior.

Do as we extractKeep as a separate task
Use input() and output() for typed child contracts. Use signals for owned UI state and computed() for derived state.A repository-wide signals or NgRx rewrite. Keep the existing user-profile store and ChatService.
Set OnPush on new components. Publish immutable model changes and fix missed updates at their source.Switching the entire app's change-detection scheduler. app.config.ts explicitly retains provideZoneChangeDetection().
Use takeUntilDestroyed() with the correct owner. Keep switchMap for request cancellation; destruction cleanup does not replace it.New signal-form or httpResource migrations. Keep existing request wrappers and follow the dynamic-form guide for supported fields.
Keep built-in control flow. Track stable IDs and use standalone deferred children for optional dependencies.An app-shell dependency cleanup or media-library replacement. Either can affect many other routes.

Signals and RxJS have different jobs here. Signals expose current state to the template. RxJS handles asynchronous selection, cancellation, and events. There is no need to replace a working request stream just to make the code look newer.

These choices follow Angular's guidance on signal inputs, outputs, OnPush, and subscription cleanup. Angular 21 defaults to zoneless, but this app opts into Zone. Evaluate an app-wide switch later, after the websocket, modal, upload, and SSR updates have explicit notifications. See Angular's zoneless migration guide.

Keep all extracted component styles in SCSS. Move each layout rule to its owning component and check encapsulation, mobile widths, and themed colors. Reuse src/app/components/ UI elements. Put labels in grouped as const files and keep endpoint and tenant URLs in existing configuration. Do not copy the current hardcoded labels or document-wide element lookups into the new components.

What each stage must prove

  • Measure cold route entry separately from warm return. Record route activation, inbox paint, history paint and composer enabled. Compare production builds on the same network and device profile, with repeated runs and p75/p95 results.
  • Check fast thread switches, overlapping searches, incoming messages during loading, empty history, failed validation, and retries. History remains readable; stale permission state never enables an action.
  • Preserve Priority / General / Bulk default selection, unread totals, direct links through ?user=, tip-back id, and history.state. Test creator, fan and delegate access.
  • Cover paid DM, remaining free credits, tip-required and guaranteed replies, locked media, shared items, mailing lists, recording, failed uploads and optimistic sends. Keep canDeactivate() attached to the queue owner; the existing guard blocks navigation when queued messages remain.
  • Compare main TopFans and one generated white-label project, including hidden inbox, owner targeting, WLSAllowDMs, tenant themes and mobile bridge file picking. Check SSR/hydration without browser globals running on the server.
  • Use --stats-json and the browser network trace to verify optional libraries load only when used. Check typing and scrolling after removing the repeated detectChanges() hook.

Build with the required --max_old_space_size=10048. npm run build-dev already supplies it. Use equivalent memory settings for production and white-label validation. Keep data-ordering and cancellation tests where behavior changes; component-count tests do not tell us whether chat still works.

The app-shell bundle cost reported by Opus can still limit cold entry after the messages route improves. Measure it separately. Do not promise a total page-load target from component-start estimates.

Code checked for this recommendation

Line references describe this audit snapshot. MC below means src/app/modules/messages/components/message-container/message-container.component.

Two implementation questions remain for the later stages. Can current websocket payloads update all inbox classifications and counts without a refetch? What are the existing history page limits and cursor rules? Confirm these before replacing the fallback or enabling pagination.

Answers to the four questions

  1. Which should we do first, breakdown or speedup?

    The small thread-loading speedup first, after a baseline. Fix cancellation and independent history/permission state, then remove the participant timer. Component extraction follows. The broader speed work continues through and after that extraction.

  2. What will the final message component tree look like?

    A thin MessageContainer with inbox, thread, composer, and an optional automated panel. The thread contains a header and timeline, the timeline renders message items, and the composer owns draft UI. Optional players, picker, recorder, and dialogs load when needed. The full tree and non-UI owners are above.

  3. Should we do both simultaneously or one by one?

    One effort with sequential, independently testable changes. Pair each component extraction with the optimization its boundary enables. Avoid a single rewrite PR and avoid concurrent edits to the monolith. Backend investigation can continue alongside the frontend work.

  4. Should Angular 21 source modernization happen now, before, or later?

    Do local modernization during extraction. Angular 21 packages and several syntax migrations are already in place. Write each new component with typed signal contracts, OnPush, stable tracking and owned cleanup. Keep zoneless, app-wide migrations and library replacements for separate work after the messages changes are stable.